たのしい人生

Scala で for 式の中括弧{} 内で <- expression が受け取れる型について

どういう経緯だったか忘れてしまったのだけれど

Haskell の do 記法は Monad 型クラスの >>= (bind?) 等に変換される糖衣構文で、do 記法で使えるのは Monad 型クラスのインスタンスを持つ型だけだけど、Scala の for 式はどういう形で受け取る型を決めているんだろう?

という話になり、「TraversableflatMap とか定義されているから Traversable が受け入れられるのでは?」と素朴に思ったのですが、Option とか Future とか明らかに違うやつがいくらでもいるので調べました。

※さらにいうと、Scala 仕様書の for のところ見ても糖衣構文だよーとかどう分解するかということまでは書いてあるものの受け入れられる型みたいな話はなかったり、"Scala for 受け入れられる型" みたいなググり方をしてもなかなかたどり着けなかったので、私みたいな検索弱者用に書く

公式ドキュメントにめっちゃ書いてあった

Scala’s “for comprehensions” are syntactic sugar for composition of multiple operations with foreach, map, flatMap, filter or withFilter. Scala actually translates a for-expression into calls to those methods, so any class providing them, or a subset of them, can be used with for comprehensions.

ただし、How does yield work? という yield に特化したタイトルで、 Scala FAQs って謎階層にいたので、みつからないよ...って思った。(Scala の Gitter チャンネルで Tsuyoshi Yoshizawa さんに教えていただいた)

ためした

object Main extends App {
  val forPo = new Forable("Po")
  
  val po = for {
    u <- forPo
  } yield u
  
  println(po)
  
  // Kaboom! Cannot compile - lack of flatMap
  // val popo = for {
  //   u <- forPo
  //   v <- forPo
  // } yield u + v
  
  val multiForPo = new MultiForable("Po")
  val multiForPoPo = new MultiForable("PoPo")
  
  val popopo = for {
    po <- multiForPo
    popo <- multiForPoPo
  } yield po + popo
  
  println(popopo)
  
  // Kaboom! Cannot compile - lack of foreach
  // for {
  //   po <- multiForPo
  //   popo <- multiForPoPo
  // } println(po + popo)
  
  val sideEffectPo = new SideEffectForable("Po")
  val sideEffectPoPo = new SideEffectForable("PoPo")
  
  for {
    po <- sideEffectPo
    popo <- sideEffectPoPo
  } println(po + popo)
  
  // Kaboom! Cannot compile - lack of filter
  // val popo = for {
  //   popo <- multiForPoPo if multiForPoPo.x == "Po"
  // } yield popo
  
  val filterPoPo = new FilterForable("PoPo")
  
  val popo = for {
    popo <- filterPoPo if filterPoPo.x == "PoPo"
  } yield popo
  
  println(popo)
}

class Forable[A](val x: A) {
  def map[B](f: A => B): Forable[B] = new Forable(f(x))
  override def toString(): String = x.toString
}

class MultiForable[A](x: A) extends Forable(x) {
  def flatMap[B](f: A => Forable[B]): Forable[B] = f(x)
}

class SideEffectForable[A](val x: A) {
  def foreach(f: A => Unit): Unit = f(x)
}

class FilterForable[A](x: A) extends MultiForable(x) {
  def withFilter(expr: A => Boolean): FilterForable[A] = this // easy implementation
}

ダラダラと書いたけど、つまるところ map とかのシグネチャを満たしてれば普通に for comprehension<- の右側に使えることがわかった。

Scala の for 式は実は単に flatMap 等のへの糖衣構文だったり明らかに Haskelldo 記法 をパクった に近いにも関わらず、Haskelldo 記法が Monad 型クラスを要求するのとは結構毛色が違う感じがする。

受け入れられるシグネチャについても結構ゆるくて

class Forable[A](val x: A) {
  def map(f: A => A): MultiForable[A] = new MultiForable(f(x))
}

とか、かなり原型をとどめていなくても行けたので、シグネチャというより、本当にメソッド名さえあっていれば良くて、脱糖されてから型検査が通ればオッケー(実際脱糖は型検査以前に行われるらしい)、という感じのようだ。

結構ふわっとした仕様でちょっと意外だった。

おしまい。

Amazon.co.jpアソシエイト