2018/04/18

 Scalaのfor式/for内包記法(For comprehension)


Scalaについて勉強した時のメモ(その4)です。for式の扱い方について。

※ 元々はmarkdownで書いていたテキストの転載。

for {
  a <- abcDao.find(id)
  b <- abcDao.find(a)
} yeild f(a, b)
  • Scalaのfor文はJavaなどと同様に、foreach文の役割を持つ。
  • Haskellのdo記法に由来するという話をどこかで読んだ気がするが出典は不明。
  • ただし、yield節を追加することで、for-yield式(for内包記法)となり、map/flatMap/filter(With)を使ったジェネレータとなる。
    • 基本的にはListのジェネレータを他のデータ型(OptionやFuture)向けに一般化したものとして考えるのが筋のような気がする (が実際のところは分からない)。
  • map/flatMap/withFilter等が定義されたデータ型に対して、これらの関数を使用したコードの別の書き方を提供する。
    • よくTwitterなどでモナドがほしいという人がいるが、実は本当に求めているのは、モナドそのものではなく、 このfor-yield風の構文(Haskellだとdo記法)の事だったりする(らしい)。。。
  • FutureやOption、Eitherなどでは、後続の処理を記述するためにmap/flatMapを使用する。 しかし、このような書き方は、ネストが深くなると、可読性が落ち、括弧の対応関係を追いづらくなる。 (いわゆるJavaScriptで言うところのcallback地獄?) また、どの結果がどの変数に代入されているかも読み取ることが難しくなる。
  • for式を導入することで、変数束縛の対応関係が明確になり、括弧の数が減少し、処理の流れが明確になる。

例えば、このネスト。

abcDao.find(id) // 戻り値はFuture[Option[String]]
  .flatMap {
    case a => a.map(f)  // aはOption[String]
      .map {
        case b => abcDao.findByName(b) // 戻り値は、Future[Option[String]]
          .map { c => (a, c) } }.getOrElse( ... ) }

for式で書き換えることで各関係が明確になる。

for {
  a <- abcDao.find(id)
  b = a.map(f)
  c <- abcDao.findByName(b)
} yield (a, c)
  • for式は、コンテクストとなる型(コンストラクタ)は必ず一つしか持てない。
    • Future用のfor式は、Future型専用、Option型のfor式は、Option型専用になる。
    • Scalazのモナド変換子だと複数のコンテクストを合成した(複数のコンテクストを持つ)新たなのコンテクストを作ることも可能。。。 例えば、FutureとEitherを組み合わせたfor式が使える: Practical Scalaz: Make async operations with scalaz.Either and Futures · KOFF.io
  • yieldの手前のfor式内で使える記法は、主に3つ。そして最後にyield節がくる。
    • flatMap: a <- b : for式と同じコンテクストの型(コンストラクタ)を持つ式(b)があり、その結果がfor式内でアンラップされた変数aに代入される。
    • map: a = b : for式が表しているコンテクストとは無関係な型を持つbを変数aに代入する。
    • filter(With): if exp : ガード節。filterとも言う。expがfalseだった場合、後続の処理を実行しない。(FutureだとFailureとなる)
    • yield(map) : yield節の式の結果をfor式の型(コンストラクタ)でラップした結果を返す。
  • 関連したテクニック
    • 式の実行結果が不要な場合は、次のようにアンダースコアを使う事で、余計な変数を避けることが出来る。
for {
  _ <- abcDao.find(id)
} yield ...

_を使うことで余計な束縛を回避している。

  • for式中の=形式の束縛は、for式内でfor式のコンテクストとは無関係な処理を実行できる。

次の書き方、

for {
  〜
  b = methodA(a)
} yield ...

は、Futureがコンテクストの場合、以下の書き方と同じ。

for {
  〜
  b <- Future { methodA(a) }
} yield ...

flatMapがmapになっていることが分かる。

  • for式はネストしたmap/flatMap/fiterWith(filter)に変換される。
    • このため、mapが複雑にネストするケースやflatMapやfilterを多用するコードは、for式の使用を検討したほうがいい。
      • プログラムがネストしすぎるのは可読性の観点から好ましくないため。
    • 一般的には、map/flatMapなどのネストよりはコードが読みやすくなる(はず)。
    • For Comprehensions and For Loops

for式がある時、

for {
  a <- abcDao.find(id)
  b <- abcDao.find(a)
} yeild f(a, b)

コンパイル時に次のように、map/flatMapに展開される。

abcDao.find(id)
  .flatMap {
    case a => abcDao.find(a)
      .map { case b => f(a, b) } }
  for式 Scala リスト内包記法