この記事は東京理科大学 Advent Calendar 2017 12/17の記事です。
はじめまして。
5年ほど前に工学部建築学科(A科)を卒業し、IT業界でScalaエンジニアをやっています。
アウトプット量を増やすべく記事を書いています。
※2017/12/31 追記しました。
背景
Scala
でループ処理や入れ子処理を書くときは、なんとなくfor式をよく使うのですが
職場の先輩より
for式はmapとflatMapのシンタックスシュガーなのでコンパイル速度とかに影響してきます。
ループが一段であればmapを使用しましょう。
と、アドバイスを頂きました。
その時はアドバイスが知的すぎて普通に理解できなかったので
今ここでこの言葉の意味をしっかり理解したいと思います。
一般的なfor式の使い方(4パターン)
そもそもfor式の一般的な使い方を理解
1.繰り返し処理を行いたい
for (変数名 ← コレクション) {
処理
}
for(x <- 1 to 3){
println(x)
}
(出力結果)
1
2
3
また、for式の(変数名 ← コレクション)をジェネレーターと呼び、コレクションから要素を一つ取り出して変数にセットします。
※ コレクションについては、以下の記事に詳しく説明されています。
Scalaのコレクションとかについて
2.フィルタリングしつつ繰り返し処理を行いたい
for (変数名 ← コレクション if 条件) {
処理
}
for(x <- 1 to 3 if x % 2 == 1 ){
println(x)
}
(出力結果)
1
3
3.繰り返し処理して新しいコレクションを作りたい
val 変数 = for (変数名 ← コレクション) yield {
処理
}
val oddVector = for(x <- 1 to 3 if x % 2 == 1 ) yield x
// => Vector(1, 3)
4.ネストしたデータ構造を繰り返し処理したい
for { 変数名 ← コレクション
変数名 ← コレクション }{
処理
}
for { x <- 1 to 2
y <- 4 to 5
}{
println(x,y)
}
(出力結果)
(1,4)
(1,5)
(2,4)
(2,5)
以上の4パターンがfor式の一般的な使い方です。
ではシンタックスシュガーになる関数を見ていきます。
mapメソッドについて
map
とは、コレクションのList
の関数です。map
メソッドを使うとList
の要素を変換して新しいListを作成できます。
val list = List(1,2,3)
val newList = list.map(_.*(2))
// => List(2, 4, 6)
flatMapメソッドについて
flatMap
も、map
と同様にコレクションのList
の関数です。flatMap
メソッドでは、List
の要素に対してmap
処理を行い、その結果を連結したList
を返します。
val list = List(1,2,3)
val newList = list.flatMap {
e => List(e, e * 5 )
}
// => List(1, 5, 2, 10, 3, 15)
以上が今回指摘された件で抑えておきます。
繰り返し構文が一段のfor式の例文
//モデル
case class Company(name: String, carModel: String)
//コレクション
val companyList = List(
Company("TOYOTA", "AQUA"),
Company("HONDA", "FIT"),
Company("SUZUKI", "SWIFT"),
Company("NISSAN", "NOTE"))
//繰り返し構文で全てのカーモデルを返す
def getAllCarModel: List[String] = {
for {
company <-companyList
} yield {
company.carModel
}
}
// getAllCarModel
// => List(AQUA, FIT, SWIFT, NOTE)
一見繰り返して使いたい時なので良いのではないかなと感じますが、
上記の内容は、for
式を使わなくてもmap
で実現できます。
def getAllCarModel: List[String] = {
companyList.map((company) => company.carModel)
}
// getAllCarModel
// => List(AQUA, FIT, SWIFT, NOTE)
確かにmap
で書いた方が簡潔です。
Scalaで繰り返し構文を実現する上でよくfor
式を使いますが、
繰り返し構文が一段の場合で、yield
を使い新しいList
を作っている処理に関してはmap
の方が良さそうですね。(ようやく先輩の言っていたことを理解しました!)
それでは
補足
Scala
では、for-yield
で書いた式はコンパイル時にmap/flatMap
を使った式に変換されます。
for-yield
を使っていると、コンパイル時にmap/flatMap
を使った式に変換されるので、
map
で最初っから記載できる単純なコードであれば、最初っからmap
で書きましょうということですね。
コンパイル速度が遅いってそういうことなんですね。
追記(2017-12-31)
速度に言及する内容を書く場合は、きちんと計測しましょうという声を頂いたので、JMHを使ってScalaでかいたコードのパフォーマンスを計測するという記事を書いて調べてみました。
※ただコンパイルの速度は調べ方がわからなかったのでコンパイルの速度は調べていません。
サンプルコード
package sample
import org.openjdk.jmh.annotations.Benchmark
class JMHSample {
@Benchmark
def measureMapMeth(): Seq[Int] = {
mapMeth(10000)
}
@Benchmark
def measureForMeth(): Seq[Int] = {
forMeth(10000)
}
def mapMeth(x: Int): Seq[Int] = {
1.to(x).map(x => x.*(2))
}
def forMeth(x: Int): Seq[Int] = {
for {
a <- 1 to x
} yield {
a.*(2)
}
}
}
JMHで確認
> jmh:run -i 20 -wi 20 -f1 -t 1
・・・
[info] Benchmark Mode Cnt Score Error Units
[info] JMHSample.measureForMeth thrpt 20 10260.024 ± 1172.508 ops/s
[info] JMHSample.measureMapMeth thrpt 20 10383.953 ± 843.856 ops/s
[success] Total time: 567 s
JMHのスループットで比較したところ、for
式とmap
関数ではほとんど差がない状態が若干map
が良いという結果でした。
しかし、Errorの範囲によってScoreの結果も変わるのでこの程度の差であれば、ほとんど性能は変わらなそうです。
繰り返し構文が一段の場合でも、コードでfor
式が多い場合はfor
式で書いた方が読みやすいと思いますし、どちらが良いか一概には言えないなと思いました。