エフェクトの作成

作成

新しいエフェクトはとても簡単にこのライブラリに追加できる。Effect 型を新しい「オプショナル型」用に作ってみよう。

必要なものは、

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

sealed trait Maybe[A]
case class Just[A](a: A) extends Maybe[A]
case class Nothing[A]() extends Maybe[A]

object MaybeEffect {
  type _maybe[R] = Maybe |= R

  def just[R :_maybe, A](a: A): Eff[R, A] =
    send[Maybe, R, A](Just(a))

  def nothing[R :_maybe, A]: Eff[R, A] =
    send[Maybe, R, A](Nothing())

  def runMaybe[R, U, A, B](effect: Eff[R, A])(implicit m: Member.Aux[Maybe, R, U]): Eff[U, Option[A]] =
    recurse(effect)(new Recurser[Maybe, U, A, Option[A]] {
      def onPure(a: A) = Some(a)

      def onEffect[X](m: Maybe[X]): Either[X, Eff[U, Option[A]]] =
        m match {
          case Just(x)   => Left(x)
          case Nothing() => Right(Eff.pure(None))
        }

      def onApplicative[X, T[_]: Traverse](ms: T[Maybe[X]]): Either[T[X], Maybe[T[X]]] =
        Right(ms.sequence)
    })

  implicit val applicativeMaybe: Applicative[Maybe] = new Applicative[Maybe] {
    def pure[A](a: A): Maybe[A] = Just(a)

    def ap[A, B](ff: Maybe[A => B])(fa: Maybe[A]): Maybe[B] =
      (fa, ff) match {
        case (Just(a), Just(f)) => Just(f(a))
        case _                  => Nothing()
      }
  }
}

上のコードでは、

コンパイラーの制限

エフェクトを作るときは、そのエフェクトで起こり得ることの区別を表現するために sealed trait と case class を定義できる。たとえばデータベースとやりとりするときはこんな風に作るだろう。

trait DatabaseEffect {

  case class Record(fields: List[String])

  sealed trait Db[A]
  case class Get[A](id: Int) extends Db[Record]
  case class Update[A](id: Int, record: Record) extends Db[Record]
}

実際は Db 型を DatabaseEffect trait の外側で作ることをおすすめする。Member implicit が解決されるときに、Db エフェクト型をどのようにインポートしているか(オブジェクトから継承しているか否か)によって、コンパイラーのクラッシュを経験するかもしれない :-(。

インタープリター

通常、与えられたエフェクトを「解釈 interpret 」するということは、型 M[X] の値に対してすべきことが分かっているということを意味する。 ここで M はエフェクトだ。もしインタープリターがエフェクトを「実行 execute 」できるのなら、つまりログを出力したり(Writer)、非同期に計算したり(Future)、値をチェックしたり(Either)できるなら、X を取り出して、そして次のエフェクトを取得して同じ用に解釈できるように継続 continuation を呼び出すことができる。

org.atnos.eff.interpret オブジェクトはインタープリターを書くためのいくつかの trait と関数を提供している。この例では Recurser を使う。これは、Maybe[X] から値 X を 「取り出す」、もしくは諦めて Eff.pure(None) を実行する。

runMaybe メソッドは Member.Aux[Maybe, R, U] 型の implicit パラメーターを必要とする。この型は次のように解釈すべきだ。


これでこのエフェクトを計算で使える。

import org.atnos.eff._
import org.atnos.eff.eff._
import MaybeEffect._

val action: Eff[Fx.fx1[Maybe], Int] =
  for {
    a <- just(2)
    b <- just(3)
  } yield a + b

run(runMaybe(action))

> Some(5)