繰り返し構文が一段の場合、for式ではなくListのmap関数を使おう

この記事は東京理科大学 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の方が良さそうですね。(ようやく先輩の言っていたことを理解しました!)

それでは

補足

gakuzzzzさんのスライドより

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式で書いた方が読みやすいと思いますし、どちらが良いか一概には言えないなと思いました。

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.