Scalaのfor式の使い方を解説

Scala 3.3.4
最終更新:2023年12月7日

[AD] Scalaアプリケーションの開発・保守は合同会社ミルクソフトにお任せください

この記事では、Scalaのfor式について解説します。

for式の概要

forというと、C言語Java言語にあるような、決められた回数を反復して処理するコードを思い浮かべる方も多いかと思います。

  • Java言語で、0 から9 まで10回繰り返す例
    Java
    for(int i = 0; i < 10; i++) { // 処理内容 }

Scalaのfor式は、単に決められた回数を反復処理するだけでなく、コレクション, Future, Option に対しても適用することができます。 さらに「式」なので、値を返します。
詳細は後ほど説明しますが、yeildキーワードの有無によって、Unitまたは、for式で処理した結果を返します。

次からは、サンプルコードでfor式の使い方を説明していきます。

for式を使ったサンプルコード

指定した回数反復処理を行う

ここでは、指定した回数(例えば、5回)反復処理する方法を説明します。

Rangeクラスを使うと、簡単に指定した回数、反復処理することができます。

  • toで指定(1 to 5)
    以下の例では、1から5まで、5回処理します。
for(i <- 1 to 5) println(i)

実行結果は、以下のようになります。

1 2 3 4 5
  • untilで指定(1 until 5)
    以下の例では、1から5 - 1 (つまり4) まで、4回処理します。
for(i <- 1 until 5) println(i)

実行結果は、以下のようになります。

1 2 3 4
  • byを指定(1 to 10 by 2)
    さらに、byを使用することで、byに指定した値おきに処理を行うこともできます。

以下の例では、1から10まで、2つおきに5回処理します。

for(i <- 1 to 10 by 2) println(i)

実行結果は、以下のようになります。

1 3 5 7 9

反復処理を入れ子にする

反復処理は、入れ子にすることができます。
以下の例では、1から9までのRangeを2つ使用して、かけ算九九の結果を表示します。

for { i <- 1 to 9 j <- 1 to 9 } print(i * j) if(j == 9) println() else print(" ,")

実行結果は、以下のようになります。

1 ,2 ,3 ,4 ,5 ,6 ,7 ,8 ,9 2 ,4 ,6 ,8 ,10 ,12 ,14 ,16 ,18 3 ,6 ,9 ,12 ,15 ,18 ,21 ,24 ,27 4 ,8 ,12 ,16 ,20 ,24 ,28 ,32 ,36 5 ,10 ,15 ,20 ,25 ,30 ,35 ,40 ,45 6 ,12 ,18 ,24 ,30 ,36 ,42 ,48 ,54 7 ,14 ,21 ,28 ,35 ,42 ,49 ,56 ,63 8 ,16 ,24 ,32 ,40 ,48 ,56 ,64 ,72 9 ,18 ,27 ,36 ,45 ,54 ,63 ,72 ,81

処理結果を取得する

これまでの例では、本体内で標準出力に出力しているだけでしたが、yieldキーワードを指定することで、 処理結果を受け取ることができます。

サンプルコードで動作を確認します。

以下のように名前(name)と、生産物の一覧(products)を持つ、生産者一覧(procedures)を定義します。

sealed abstract class Fruit object Fruit: object Apple extends Fruit object Orange extends Fruit object Banana extends Fruit object Grape extends Fruit object Mango extends Fruit import Fruit.* // 生産者 case class Producer( name: String, // 名前 products: Seq[Fruit] // 生産物一覧 ) // 生産者一覧 def producers = Seq( Producer("Taro", Seq(Apple, Orange)), Producer("Hanako", Seq(Apple, Banana, Grape)), Producer("Ichiro", Seq(Mango)), )

以下の例は、上記、生産者一覧(producers)から名前(name)の一覧を取得しています。

val names: Seq[String] = for { producer <- producers } yield producer.name println(names.mkString(", "))

実行結果は、以下のようになります。

Taro, Hanako, Ichiro

フィルターを利用する

フィルターを使用することで、条件に該当する場合のみ本体を実行することができます。

以下の例では、前述の生産者一覧(producers)から、Appleを生産物(products)に含む生産者の名前を取得しています。

val names: Seq[String] = for { producer <- producers if(producer.products.contains(Apple)) } yield producer.name println(names.mkString(", "))

実行結果は、以下のようになります。

Taro, Hanako

処理結果を変数に束縛する

for式内で計算結果を複数の箇所で使用する場合など、計算量を削減するために、中間結果を変数に束縛することができます。

以下の例では、生産者の名前から郵便番号を取得する関数(getZipcode)の結果を変数cに束縛しています。

val result = for { producer <- producers c = getZipcode(producer.name).getOrElse("") if(!c.isEmpty()) } yield (producer.name, c) println(result.mkString(", "))

実行結果は、以下のようになります。

(Taro,1112222), (Hanako,3334444)

Optionに対してfor式を適用する

for式の概要 で説明したとおり、Optionに対してもfor式を適用することができます。

以下の例では、2つのOption[Int]から値を取り出して、足した値をSomeにくるんで返しています。

val o1: Option[Int] = Some(1) val o2: Option[Int] = Some(2) val result = for { r1 <- o1 r2 <- o2 } yield r1 + r2 println(result.getOrElse("Can not sum"))

実行結果は、以下のようになります。

3

上記の例では、すべてのOption[Int]が、値を持つSomeでしたが、Noneが含まれる場合は、どのように処理されるのでしょうか?
以下の例では、3つのOption[Int]の値を合計していますが、Noneが含まれています。
この場合、Noneが現れたところで処理が中断され、for式が返す値も、Noneになります。

val o1: Option[Int] = Some(1) val o2: Option[Int] = None val o3: Option[Int] = Some(2) val result = for { r1 <- o1 r2 <- o2 r3 <- o3 } yield r1 + r2 + r3 println(result.getOrElse("Can not sum"))

実行結果は、以下のようになります。

Can not sum

for式を構成する要素の説明

ここでは、for式を構成する要素について説明します。
for式の基本的な構文は、以下のように記載します。

for(ジェネレーター; 定義; フィルター, ...) 本体

ジェネレーター

ジェネレーターは、p <- exprのように記述します。
ジェネレーターのexprには、これまでに説明したように、コレクション、FutureOption等が指定できます。
pは、exprの要素を束縛します。

定義

定義は、x = exprのように記載すると、exprの値をxに束縛し、変数を定義することができます。

フィルター

フィルターは、if exprのように記載します。
フィルターのexprは、Boolean形式の式です。
フィルターは、exprfalseの場合、その要素を処理しません。

本体

本体は、yieldキーワードを指定する場合と指定しない場合があります。
for式は、yieldキーワードを指定した場合は、本体が返した値を要素に持つコレクションを返し、yieldキーワードを指定しない場合は、Unit値を返します。

中括弧を使用した記述方法

for式は、少括弧()の代わりに、中括弧{}を使用して記載することもできます。

for { ジェネレーター 定義 フィルター ... } 本体

中括弧{}を使用する場合は、ジェネレーター、定義、フィルターの後ろのセミコロン;は、記述する必要はありません。

for式の動作

ここでは、for式が、どのように実行されるのかを説明します。 Scalaのfor式は、シンタックスシュガーであり、以下のメソッドに展開されて実行されます。
したがって、以下のメソッドを実装したクラスであれば、for式で処理することができます。

  • foreach
  • map
  • flatMap
  • withFilter

では、for式を使った以下のコードが、どのように展開されるかを見てみます。

val names: Seq[String] = for { producer <- producers if(producer.products.contains(Apple)) } yield producer.name println(names.mkString(", "))

上記のコードは、以下のように、withFiltermapに展開して実行されます。

val names: Seq[String] = producers.withFilter( producer => producer.products.contains(Apple)).map(producer => producer.name) println(names.mkString(", "))

詳細な変換方法の説明は、本記事では割愛しますが、おおむね以下のように覚えておくと良いでしょう。
yieldキーワードがつく場合は、flatMap, mapに変換
yieldキーワードがつかない場合は、foreachに変換
フィルターは、withFilterに変換

反復処理に関するその他の機能

index付きの反復処理について

コレクションを反復処理するときに、インデックスが必要となる場合があります。
Scalaのコレクションには、インデックスを付与する便利なメソッド(zipWithIndex)が用意されています。

以下の例では、生産者一覧(producers)に対して、zipWithIndexメソッドを呼び出し、インデックスを付与しています。

producers.zipWithIndex.foreach { case (producer, i) => println(s"${i + 1}: ${producer.name}")}

実行結果は、以下のようになります。

1: Taro 2: Hanako 3: Ichiro

反復処理本体からの脱出方法について

他の言語(例えば、Java言語)のように、反復処理内から脱出することができます。

breakを使用するには、Breaksをインポートする必要があります。

import scala.util.control.Breaks

以下の例では、bananaが現れた場合に、for式の本体から脱出します。

val b = new Breaks() var name: Option[String] = None b.breakable: for(producer <- producers) if(producer.products.contains(Banana)) name = Some(producer.name) b.break println(name.getOrElse(""))

実行結果は、以下のようになります。

Hanako

ただし、breakを利用した場合、反復処理の処理結果を返すことができないなどの弊害があるため、他の方法を使用することをおすすめします。

上記例は、findメソッドを使用して以下のように書き換えることができます。

val name = producers.find(p => p.products.contains(Banana)) match case Some(p) => p.name case _ => "" println(name)

サイト内検索