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._
import 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](effect: Eff[R, A])(using m: Member.Aux[Maybe, R, U]): Eff[U, Option[A]] =
recurse(effect)(new Recurser[Maybe, U, A, Option[A]] {
def onPure(a: A): Option[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)
})
given 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: ``
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
UThen we can use this effect in a computation:`>
Some(5)`