チュートリアル

このチュートリアルは意図して Free monad tutorial for cats と同じように構造化している。そのため両者のアプローチを並べて比較できる。

自分のテーマを深く知る

キーバリューストアのための DSL を作りたいとしよう。キーを使って3つのことができるようにしたい。


考えとしては、組み込み DSL でこれら一連の操作を「プログラム」として書き、その「プログラム」を解釈 interpret し、そして最後に実際のキーバリューストアと作用させるように「プログラム」を実行したい。

例えば、

 put("toto", 3)
 get("toto") // 3 が返る
 delete("toto")

だが欲しいのは、

自分の文法を表す ADT を作る

ADT とは代数的データ型(Algebraic Data Type)を表す。今回の文脈では、複合的で再帰的な値を構築するために組み合わせて使える、型の閉じた集合のことだ。

私たちのキーバリュー操作を表す ADT を作る必要がある。

// 型パラメーター A は操作が返す値の型として読める
sealed trait KVStore[+A]

case class Put[T](key: String, value: T) extends KVStore[Unit]
case class Get[T](key: String) extends KVStore[Option[T]]
case class Delete(key: String) extends KVStore[Unit]

ADT を自由にする

ADT を「自由にする」には4つの基本ステップがある。

  1. Eff.send を使うことで、KVStore[_] 用のスマートコンストラクタを作る
  2. キーバリュー DSL 操作用のプログラムを作る
  3. DSL 操作のプログラム用のインタープリターを作る
  4. インタープリターが解釈したプログラムを実行する
  5. [任意] インタープリター用のシンタックスを追加する

Eff.send でスマートコンストラクタを作る

これらのメソッドで、キーバリューストア「エフェクト」用に Eff の値を作ることができる。

import org.atnos.eff._

// T |= R は MemberIn[T, R] のエイリアスであり、
// 型 T[_] のエフェクトをエフェクトスタック R に注入できることを宣言する。
// 以下は MemberIn[KVStore, R] と等価でもある
type _kvstore[R] = KVStore |= R

/** store は何も返さない(Unit) */
def store[T, R :_kvstore](key: String, value: T): Eff[R, Unit] =
  Eff.send[KVStore, R, Unit](Put(key, value))

/** find はキーが存在すれば T の値を返す */
def find[T, R :_kvstore](key: String): Eff[R, Option[T]] =
  Eff.send[KVStore, R, Option[T]](Get(key))

/** delete は何も返さない(Unit) */
def delete[T, R :_kvstore](key: String): Eff[R, Unit] =
  Eff.send(Delete(key))

/** update は get と put を合成し、何も返さない */
def update[T, R :_kvstore](key: String, f: T => T): Eff[R, Unit] =
  for {
    ot <- find[T, R](key)
    _  <- ot.map(t => store[T, R](key, f(t))).getOrElse(Eff.pure(()))
  } yield ()

それぞれのメソッドは KVStore エフェクトが「エフェクトスタック」 R のメンバーであることを要求し、戻り値は Eff[R, A] 型だ。ここで R はエフェクトのスタックであり、キーバリューストア操作以外のエフェクトを含んでいるかもしれず、型 A の値を生成する。

プログラムを作る

これで KVStore エフェクトで値を生成でき、for 内包表記を使うことで「プログラム」を書くための DSL が使える。

import org.atnos.eff._

def program[R :_kvstore]: Eff[R, Option[Int]] =
  for {
    _ <- store("wild-cats", 2)
    _ <- update[Int, R]("wild-cats", _ + 12)
    _ <- store("tame-cats", 5)
    n <- find[Int, R]("wild-cats")
    _ <- delete("tame-cats")
  } yield n

これはモナドの流れのように見える。だが、一連の操作を表す再帰的なデータ構造を組み立てているだけだ。

プログラム用のインタープリターを書く

もう理解できただろうが、Eff は埋め込み DSL を使うために使える。それ単独では、この DSL は(再帰的データ構造で定義された)一連の操作を表現するだけだ。何も生成しない。

Eff はプログラミング言語内のプログラミング言語なのだ!

なので、他のプログラミング言語と同様に、われわれの抽象的な言語を具体的な値へと解釈する必要がある。

これをするためには、単純なミュータブルなマップを使って KVStore エフェクトを変換するインタープリターを使う。

import org.atnos.eff._, interpret._
import cats.Traverse
import cats.implicits._
import scala.collection.mutable._


/**
 * KVStore エフェクトの安全でないインタープリター
 *
 * 間違った型が指定されたらプログラムはクラッシュするだろう。
 *
 * このインタープリターは KVStore エフェクトが R の「メンバーである(<=)」ことを要求する。
 * つまり、R から KVStore を取り除いたら結果の型が m.Out であると静的に知ることができる。
 *
 * インタープリターは `org.atnos.eff.Interpreter` にある `interpretUnsafe` メソッドを使うことで、
 * スタック安全なエフェクトの解釈を副作用として実装する。
 *
 * `interpretUnsafe` はそれぞれの `KVStore[X]` エフェクトを得る副作用の定義を要求し、副作用を実行して、値 `X` を返す。
 *
 * 結果として生まれるエフェクトスタックは m.Out であり、KVStore エフェクトを含まない R となる
 *
 */
def runKVStoreUnsafe[R, A](effects: Eff[R, A])(implicit m: KVStore <= R): Eff[m.Out, A] = {
  // 超単純(で不正確な)キーバリューストア
  val kvs = Map.empty[String, Any]

  val sideEffect = new SideEffect[KVStore] {
    def apply[X](kv: KVStore[X]): X =
      kv match {
        case Put(key, value) =>
          println(s"put($key, $value)")
          kvs.put(key, value)
          ().asInstanceOf[X]

        case Get(key) =>
          println(s"get($key)")
          kvs.get(key).asInstanceOf[X]

        case Delete(key) =>
          println(s"delete($key)")
          kvs.remove(key)
          ().asInstanceOf[X]
      }

    def applicative[X, Tr[_] : Traverse](ms: Tr[KVStore[X]]): Tr[X] =
      ms.map(apply)
  }
  interpretUnsafe(effects)(sideEffect)(m)

}

このインタープリターは純粋でないことに注意してほしい。kvs を変化させており、println を使ってログ出力を生成している。関数プログラミングの真の目的は副作用を防ぐことではない。よく知られた、管理された方法で、副作用を、あなたのシステムと外部の境界に追いやることなのだ。

違う方法で KVStore エフェクトを解釈し、同じスタックの他のエフェクトに結果を委託することもできる。


import org.atnos.eff._
import org.atnos.eff.either._
import org.atnos.eff.writer._
import org.atnos.eff.state._
import org.atnos.eff.interpret._
import cats.implicits._
import cats.data._

type _writerString[R] = Writer[String, *] |= R
type _stateMap[R]     = State[Map[String, Any], *] |= R

/**
 * KVStore エフェクトの安全なインタープリター
 *
 * 次のエフェクトを使っている。
 *
 * - Writer はログ出力のため
 * - State はキーバリュー Map を更新するため
 * - Either は map 内のオブジェクトの型が期待する型でないときにエラーを発生させるため
 *
 * 結果として生じるエフェクトスタックは U であり、R から KVStore エフェクトを除いたものだ。
 *
 * スタック U 内で作れるようになっているエフェクトは Throwable、Writer そして State エフェクト
 * だけであることに気づいてほしい。
 *
 * このインタープリターは org.atnos.eff.interpreter.translate メソッドを使っており、
 * スタックのエフェクトを同じスタックの他のエフェクトに変換している。
 *
 *
 * 備考:
 * - 型推論のために U のエフェクトが R のエフェクトより後ろに並んでいることはとても重要!
 *
 * Implicit の Member 定義は以下の定義では見つからない。
 *
 * def runKVStore[R, U :_throwableEither :_writerString :_stateMap, A](effects: Eff[R, A]) (
 *   implicit m: Member.Aux[KVStore, R, U]): Eff[U, A] = {
 *
 */
def runKVStore[R, U, A](effects: Eff[R, A])
  (implicit m: Member.Aux[KVStore, R, U],
            throwable:_throwableEither[U],
            writer:_writerString[U],
            state:_stateMap[U]): Eff[U, A] = {

  translate(effects)(new Translate[KVStore, U] {
    def apply[X](kv: KVStore[X]): Eff[U, X] =
      kv match {
        case Put(key, value) =>
          for {
            _ <- tell(s"put($key, $value)")
            _ <- modify((map: Map[String, Any]) => map.updated(key, value))
            r <- fromEither(Either.catchNonFatal(().asInstanceOf[X]))
          } yield r

        case Get(key) =>
          for {
            _ <- tell(s"get($key)")
            m <- get[U, Map[String, Any]]
            r <- fromEither(Either.catchNonFatal(m.get(key).asInstanceOf[X]))
          } yield r

        case Delete(key) =>
          for {
            _ <- tell(s"delete($key)")
            u <- modify((map: Map[String, Any]) => map - key)
            r <- fromEither(Either.catchNonFatal(().asInstanceOf[X]))
          } yield r
      }
  })
}

Eff はただの再帰的な構造であり、副作用があるかもしれない、他の操作を生成する操作の連続としてとらえることができる。この場合、List の畳み込みと似ている。われわれはよくリストから1つの値を得るために畳み込み(例えば foldRight)を使う。これは構造を再帰的に処理し、中身を結合していく。

Eff インタープリターの背景にある考え方はまったく同じだ。次の操作で再帰的構造を「畳み込む」。


インタープリターの重要な特徴はスタック安全であることだ。インタープリターはスタック上の計算の各ステップを評価してから、他のステップを評価するために自分自身を呼び出す。org.atnos.eff.interpreter オブジェクトはスタック安全なインタープリターを書くのを助けてくれる色々なメソッドを提供している。

プログラムを実行する

最後のステップは当然、他の Eff の値にひもづけてプログラムを解釈してから、プログラムを実行することだ。やるべきことは、

こんな感じだ。

import org.atnos.eff._, syntax.all._

// 安全でないインタープリターでプログラムを実行
runKVStoreUnsafe(program[Fx.fx1[KVStore]]).run

> Some(14)

安全なインタープリターを使うと、プロセスは同様でやるべきことは、

こんな感じだ。

import org.atnos.eff._, syntax.all._
import cats._, data._

// 安全なインタープリターでプログラムを実行
type Stack = Fx.fx4[KVStore, Either[Throwable, *], State[Map[String, Any], *], Writer[String, *]]

val (result, logs) =
  runKVStore(program[Stack]).runEither.evalState(Map.empty[String, Any]).runWriter.run

(result.toString +: logs).mkString("\n")
> Right(Some(14))
put(wild-cats, 2)
get(wild-cats)
put(wild-cats, 14)
put(tame-cats, 5)
get(wild-cats)
delete(tame-cats)

シンタックスを追加する

すばらしいことに、シンタックス用の次の追加コードで run メソッドをチェーン(数珠つなぎ)していけるのだ。

implicit class KVStoreOps[R, A](effects: Eff[R, A]) {
  def runStore[U](implicit m: Member.Aux[KVStore, R, U],
                  throwable:_throwableEither[U],
                  writer:_writerString[U],
                  state:_stateMap[U]): Eff[U, A] =
    runKVStore(effects)
}

val (result, logs) =
  program[Stack].runStore.runEither.evalState(Map.empty[String, Any]).runWriter.run

(result.toString +: logs).mkString("\n")
> Right(Some(14))
put(wild-cats, 2)
get(wild-cats)
put(wild-cats, 14)
put(tame-cats, 5)
get(wild-cats)
delete(tame-cats)

Eff モナドで ADTs を合成する

現実世界のアプリケーションは異なる代数をしばしば合成する。Eff[R, A] におけるエフェクトの型レベル集合 R のおかげで、 Eff のコンテキストで異なる代数を合成できる。

無関係の ADT が合成されてより複雑なプログラムを形成している、ちょっとした例を見てみよう。まず ADT をスマートコンストラクタで定義する。

import org.atnos.eff._, all._

sealed trait Interact[A]

case class Ask(prompt: String) extends Interact[String]
case class Tell(msg: String) extends Interact[Unit]

type _interact[R] = Interact |= R

def askUser[R :_interact](prompt: String): Eff[R, String] =
  send(Ask(prompt))

def tellUser[R :_interact](message: String): Eff[R, Unit] =
  send(Tell(message))

sealed trait DataOp[A]

type _dataOp[R] = DataOp |= R

case class AddCat(a: String) extends DataOp[Unit]
case class GetAllCats() extends DataOp[List[String]]

def addCat[R :_dataOp](a: String): Eff[R, Unit] =
  send(AddCat(a))

def getAllCats[R :_dataOp]: Eff[R, List[String]] =
  send(GetAllCats())

それから、これらエフェクトについてプログラム上で MemberIn インスタンスを用意する必要がある。

import org.atnos.eff._

def program[R :_interact :_dataOp]: Eff[R, Unit] =
  for {
    cat  <- askUser("What's the kitty's name?")
    _    <- addCat(cat)
    cats <- getAllCats
    _    <- tellUser("Current cats: "+cats.mkString(", "))
  } yield ()

最後に ADT ごとにひとつずつインタープリターを書く。

import cats._
import cats.implicits._
import org.atnos.eff._, interpret._

def readLine(): String =
  "snuggles"

def runInteract[R, A](effect: Eff[R, A])(implicit m: Interact <= R): Eff[m.Out, A] =
  recurse(effect)(new Recurser[Interact, m.Out, A, A] {
    def onPure(a: A): A = a

    def onEffect[X](i: Interact[X]): Either[X, Eff[m.Out, A]] = Left {
      i match {
        case Ask(prompt) =>
          println(prompt)
          readLine()

        case Tell(msg) =>
          println(msg)
      }
    }

    def onApplicative[X, T[_] : Traverse](ms: T[Interact[X]]): Either[T[X], Interact[T[X]]] =
      Left(ms.map {
        case Ask(prompt) => println(prompt); readLine()
        case Tell(msg)   => println(msg)
      })

  })(m)

def runDataOp[R, A](effect: Eff[R, A])(implicit m: DataOp <= R): Eff[m.Out, A] = {
  val memDataSet = new scala.collection.mutable.ListBuffer[String]

  recurse(effect)(new Recurser[DataOp, m.Out, A, A] {
    def onPure(a: A): A = a

    def onEffect[X](i: DataOp[X]): Either[X, Eff[m.Out, A]] = Left {
      i match {
        case AddCat(a)    => memDataSet.append(a); ()
        case GetAllCats() => memDataSet.toList
      }
    }

    def onApplicative[X, T[_]: Traverse](ms: T[DataOp[X]]): Either[T[X], DataOp[T[X]]] =
      Left(ms.map {
        case AddCat(a)    => memDataSet.append(a); ()
        case GetAllCats() => memDataSet.toList
      })
  })(m)

}

両方のエフェクトを合成したスタックを使ってプログラムを実行し、入力指示に "snuggles" と入力したら、こんな結果を見るだろう。

type Stack = Fx.fx2[Interact, DataOp]

runInteract(runDataOp(program[Stack]))
What's the kitty's name?
Current cats: snuggles