1. do vs for

do vs for 

Haskell の do 記法と Scala の for 内包表記には微妙な違いがある。以下が do 記法の例:

foo = do
  x <- Just 3
  y <- Just "!"
  Just (show x ++ y)

通常は return (show x ++ y) と書くと思うけど、最後の行がモナディックな値であることを強調するために Just を書き出した。一方 Scala はこうだ:

def foo = for {
  x <- Some(3)
  y <- Some("!")
} yield x.toString + y

似ているように見えるけども、いくつかの違いがある。

  • Scala には標準で Monad 型が無い。その代わりにコンパイラが機械的に for 内包表記を mapwithFilterflatMapforeach の呼び出しに展開する。 SLS 6.19
  • OptionList など、標準ライブラリが map/flatMap を実装するものは、Cats が提供する型クラスよりも組み込みの実装が優先される。
  • Scala collection ライブラリの map その他は F[A]G[B] に変換する CanBuildFrom を受け取る。Scala コレクションのアーキテクチャ 参照。
  • CanBuildFromG[A] から F[B] という変換を行うこともある。
  • pure 値を伴う yield を必要とする。さもないと、forUnit を返す。

具体例を見てみよう:

import collection.immutable.BitSet

val bits = BitSet(1, 2, 3)
// bits: BitSet = BitSet(1, 2, 3)

for {
  x <- bits
} yield x.toFloat
// res0: collection.immutable.SortedSet[Float] = TreeSet(1.0F, 2.0F, 3.0F)

for {
  i <- List(1, 2, 3)
  j <- Some(1)
} yield i + j
// res1: List[Int] = List(2, 3, 4)

for {
  i <- Map(1 -> 2)
  j <- Some(3)
} yield j
// res2: collection.immutable.Iterable[Int] = List(3)

actM を実装する 

Scala には、マクロを使って命令型的なコードをモナディックもしくは applicative な関数呼び出しに変換している DSL がいくつか既にある:

Scala 構文の全域をマクロでカバーするのは難しい作業だけども、 Async と Effectful のコードをコピペすることで単純な式と val のみをサポートするオモチャマクロを作ってみた。 詳細は省くが、ポイントは以下の関数だ:

  def transform(group: BindGroup, isPure: Boolean): Tree =
    group match {
      case (binds, tree) =>
        binds match {
          case Nil =>
            if (isPure) q"""$monadInstance.pure($tree)"""
            else tree
          case (name, unwrappedFrom) :: xs =>
            val innerTree = transform((xs, tree), isPure)
            val param = ValDef(Modifiers(Flag.PARAM), name, TypeTree(), EmptyTree)
            q"""$monadInstance.flatMap($unwrappedFrom) { $param => $innerTree }"""
        }
    }

actM を使ってみよう:

import cats._, cats.syntax.all._
import example.MonadSyntax._

actM[Option, String] {
  val x = 3.some.next
  val y = "!".some.next
  x.toString + y
}
// res3: Option[String] = Some(value = "3!")

fa.nextMonad[F].flatMap(fa)() の呼び出しに展開される。 そのため、上の例はこのように展開される:

Monad[Option].flatMap[String, String]({
  val fa0: Option[Int] = 3.some
  Monad[Option].flatMap[Int, String](fa0) { (arg0: Int) => {
    val next0: Int = arg0
    val x: Int = next0
    val fa1: Option[String] = "!".some
    Monad[Option].flatMap[String, String](fa1)((arg1: String) => {
      val next1: String = arg1
      val y: String = next1
      Monad[Option].pure[String](x.toString + y)
    })
  }}
}) { (arg2: String) => Monad[Option].pure[String](arg2) }
// res4: Option[String] = Some(value = "3!")

Option から List への自動変換を防止できるか試してみる:

{
  actM[List, Int] {
    val i = List(1, 2, 3).next
    val j = 1.some.next
    i + j
  }
}
// error: Option[String] does not take parameters
// Monad[Option].flatMap[String, String]({
// ^

エラーメッセージがこなれないけども、コンパイル時に検知することができた。 これは、Future を含むどのモナドでも動作する。

val x = {
  import scala.concurrent.{ExecutionContext, Future}
  import ExecutionContext.Implicits.global
  actM[Future, Int] {
    val i = Future { 1 }.next
    val j = Future { 2 }.next
    i + j
  }
}
// x: concurrent.Future[Int] = Future(Success(3))

x.value
// res6: Option[util.Try[Int]] = None

このマクロは不完全な toy code だけども、こういうものがあれば便利なのではという示唆はできたと思う。