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 はこうだ:

scala> def foo = for {
         x <- Some(3)
         y <- Some("!")
       } yield x.toString + y
foo: Option[String]

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

具体例を見てみよう:

scala> import collection.immutable.BitSet
import collection.immutable.BitSet

scala> val bits = BitSet(1, 2, 3)
bits: scala.collection.immutable.BitSet = BitSet(1, 2, 3)

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

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

scala> for {
         i <- Map(1 -> 2)
         j <- Some(3)
       } yield j
res2: scala.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 を使ってみよう:

scala> import cats._, cats.data._, cats.implicits._
import cats._
import cats.data._
import cats.implicits._

scala> import example.MonadSyntax._
import example.MonadSyntax._

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

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

scala> 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(3!)

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

scala> actM[List, Int] {
         val i = List(1, 2, 3).next
         val j = 1.some.next
         i + j
       }
<console>:25: error: exception during macro expansion:
scala.reflect.macros.TypecheckException: type mismatch;
 found   : fa$macro$15.type (with underlying type Option[Int] @scala.reflect.internal.annotations.uncheckedBounds)
 required: List[?]
	at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2$$anonfun$apply$1.apply(Typers.scala:34)
	at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2$$anonfun$apply$1.apply(Typers.scala:28)
	at scala.reflect.macros.contexts.Typers$$anonfun$3.apply(Typers.scala:24)
	at scala.reflect.macros.contexts.Typers$$anonfun$3.apply(Typers.scala:24)
	at scala.reflect.macros.contexts.Typers$$anonfun$withContext$1$1.apply(Typers.scala:25)
	at scala.reflect.macros.contexts.Typers$$anonfun$withContext$1$1.apply(Typers.scala:25)
	at scala.reflect.macros.contexts.Typers$$anonfun$1.apply(Typers.scala:23)
	at scala.reflect.macros.contexts.Typers$$anonfun$1.apply(Typers.scala:23)
	at scala.reflect.macros.contexts.Typers$class.withContext$1(Typers.scala:25)
	at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2.apply(Typers.scala:28)
	at scala.reflect.macros.contexts.Typers$$anonfun$typecheck$2.apply(Typers.scala:28)
	at scala.reflect.internal.Trees$class.wrappingIntoTerm(Trees.scala:1716)
	at scala.reflect.internal.SymbolTable.wrappingIntoTerm(SymbolTable.scala:16)
	at scala.reflect.macros.contexts.Typers$class.withWrapping$1(Typers.scala:26)
	at scala.reflect.macros.contexts.Typers$class.typecheck(Typers.scala:28)
	at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
	at scala.reflect.macros.contexts.Context.typecheck(Context.scala:6)
	at example.internal.ActMTransform$class.actMTransform(ActMTransform.scala:27)
	at example.internal.ActMMacro$$anon$1.actMTransform(ActMMacro.scala:8)
	at example.internal.ActMBase.actMImpl(ActMBase.scala:13)
	at example.internal.ActMImpl$.actMImpl(ActMImpl.scala:9)

       actM[List, Int] {
                       ^

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

scala> :paste
// Entering paste mode (ctrl-D to finish)
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
  }
}

// Exiting paste mode, now interpreting.

x: scala.concurrent.Future[Int] = List()

scala> x.value
res6: Option[scala.util.Try[Int]] = Some(Success(3))

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

Contents