[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
には、これまでに説明したように、コレクション、Future
、Option
等が指定できます。
p
は、expr
の要素を束縛します。
定義
定義は、x = expr
のように記載すると、expr
の値をx
に束縛し、変数を定義することができます。
フィルター
フィルターは、if expr
のように記載します。
フィルターのexpr
は、Boolean
形式の式です。
フィルターは、expr
がfalse
の場合、その要素を処理しません。
本体
本体は、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(", "))
上記のコードは、以下のように、withFilter
とmap
に展開して実行されます。
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)