このチュートリアルは意図して Free monad tutorial for cats と同じように構造化している。そのため両者のアプローチを並べて比較できる。
キーバリューストアのための DSL を作りたいとしよう。キーを使って3つのことができるようにしたい。
考えとしては、組み込み DSL でこれら一連の操作を「プログラム」として書き、その「プログラム」を解釈 し、そして最後に実際のキーバリューストアと作用させるように「プログラム」を実行したい。
例えば、
put("toto", 3)
get("toto") // 3 が返る
delete("toto")
だが欲しいのは、
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 を「自由にする」には4つの基本ステップがある。
Eff.send
を使うことで、KVStore[_]
用のスマートコンストラクタを作る
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
エフェクトを解釈し、同じスタックの他のエフェクトに結果を委託することもできる。
State
は値のマップ構造を保持するため
Writer
はログのためEither[E, *]
は型エラーのため
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
オブジェクトはスタック安全なインタープリターを書くのを助けてくれる色々なメソッドを提供している。
interpretUnsafe
を使うには
SideEffect
trait
を実装し、エフェクト T[X]
から値
X
を返させる
translate
を使うには
Translate
trait
を実装し、エフェクトを同じスタックの他のエフェクトに変換させる
どちらも
interpret1
の特別版である。それを使うには
Recurse
trait
を実装し、エフェクトから値
X
を返すか、または他の
Eff
の計算を生成させる
最後のステップは当然、他の
Eff
の値にひもづけてプログラムを解釈してから、プログラムを実行することだ。やるべきことは、
Fx.fx1[KVStore]
(今回のスタックではエフェクトは1つだけ)を含んでいるエフェクトの具体的なスタックを指定する
Eff[NoFx, A]
値を取得するインタープリターを呼ぶ
A
の値を得るために最後に
run
を実行する
こんな感じだ。
import org.atnos.eff._, syntax.all._
// 安全でないインタープリターでプログラムを実行
runKVStoreUnsafe(program[Fx.fx1[KVStore]]).run
> Some(14)
安全なインタープリターを使うと、プロセスは同様でやるべきことは、
NoFx
エフェクトの
run
こんな感じだ。
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[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