New effects can be added to the library pretty easily. Let’s create an Effect for a new “optional” type.
We need:
a base type. We use a Maybe
data type with 2 cases Just
and Nothing
a method to send values of type A
into Eff[R, A]
an interpreter
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]): X Either 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]]): T[X] Either 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()
}
}
}
In the code above:
the just
and nothing
methods use Eff.send
to “send” values into a larger sum of effects Eff[R, A]
runMaybe
runs the Maybe
effect by using the interpret.recurse
and a Recurser
to translate Maybe
values into Option
values
When you create an effect you can define a sealed trait and case classes to represent different possibilities for that effect. For example for interacting with a database you might create:
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]
}
It is recommended to create the Db
types outside of the DatabaseEffect
trait. Indeed, during Member
implicit resolution, depending on how you import the Db
effect type (if it is inherited from an object or not) you could experience compiler crashes :-(.
Interpreting a given effect generally means knowing what to do with a value of type M[X]
where M
is the effect. If the interpreter can “execute” the effect: produce logs (Writer
), execute asynchronously (Future
), check the value (Either
),… then extract a value X
, then we can call a continuation to get the next effect and interpret it as well.
The org.atnos.eff.interpret
object offers several support traits and functions to write interpreters. In this example we use a Recurser
which will be used to “extract” a value X
from Maybe[X]
or just give up with Eff.pure(None)
The runMaybe
method needs an implicit Member.Aux[Maybe, R, U]
. This must be read in the following way:
Maybe
must be member of the effect stack R
and its removal from R
should be the effect stack U
Then we can use this effect in a computation:
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)