How do I chain action and interpret them together with Scalaz?

83 views Asked by At

I am trying to learn how to use FreeMonads to implement interpreters for my services.

Suppose I have

sealed trait ServiceAction[T] extends Product with Serializable
case class ConsumeCommand(cmd: AccruePoints) extends ServiceAction[AccruePointModel]
case class CreateEvent(evt: PointsAccruedEvent) extends ServiceAction[PointsAccruedEvent]

sealed trait LogAction[T] extends Product with Serializable
case class Info(msg: String) extends LogAction[Unit]
case class Error(msg: String) extends LogAction[Unit]

and a Monad of the action

type LogActionF[A] = Free[LogAction, A]
type ServiceActionF[A] = Free[ServiceAction, A]

Next, I define my service like this:

trait PointAccrualService {
    def consume(cmd: AccruePoints): ServiceActionF[AccruePointModel] = Free.liftF(ConsumeCommand(cmd))
    def emit(evt: PointsAccruedEvent) : ServiceActionF[PointsAccruedEvent] = Free.liftF(CreateEvent(evt))
}

and

trait LogService {
  def info(msg: String) : LogActionF[Unit] = Free.liftF(Info(msg))
  def error(msg: String) : LogActionF[Unit] = Free.liftF(Error(msg))
}

with an object of each

object LogService extends LogService
object PointAccrualService extends PointAccrualService

My LogServiceInterpreter is like this:

case class LogServiceConsoleInterpreter() extends LogServiceInterpreter {
  def apply[A](action: LogActionF[A]): Task[A] = action.foldMap(handler)             

    protected def handler = new (LogAction ~> Task) {
    override def apply[A](fa: LogAction[A]) = fa match {
      case Info(m) =>
        now(info(m))
      case Error(m) =>
        now(error(m))
    }
  }

  def info(msg: String): Unit = {
    println(s"INFO: $msg")
  }

  def error(msg: String): Unit = {
    println(s"ERROR: $msg")
  }
}

Similarly, my PointAccuralServiceInterpreter is like this:

case class PointAccuralServiceInterpreter() {
  def apply[A] (action: ServiceActionF[A]) : Task[A] = action.foldMap(handler)
  protected def handler = new (ServiceAction ~> Task) {
    override def apply[A](fa: ServiceAction[A]): Task[A] = fa match {
      case ConsumeCommand(cmd) => {
        println("Service ConsumeCommand:" + cmd)
        now(cmd)
      }
      case CreateEvent(evt) => {
        println("Service CreateEvent:" + evt)
        now(evt)
      }
    }
  }
}

My logic is straightforward, I want to log, and consume my command and then create an event, sort of like an event sourcing:

val ret = for {
  _ <- logService.info("Command: " + cmd)
  model <- service.consume(cmd)
  _ <- logService.info("Model: " + model)
  evt <- service.emit(model.toEvent("200", "Event Sent"))
  _ <- logService.info("Event:" + evt)
} yield evt

This code doesn't even compile actually.

What should I do from here? I think I am supposed to use Coproduct to chain them and execute this piece of logic by feeding my interpreter.

I found something here https://groups.google.com/forum/#!topic/scalaz/sHxFsFpE86c

or it's said I can use Shapeless to do so Folding a list of different types using Shapeless in Scala

They are all too complicated. All I want is, after I define my logic, how do I execute it?

Hope I put enough details here for an answer. I really want to learn this. Thanks

1

There are 1 answers

0
Gunnar Dietz On BEST ANSWER

I slightly modified your code to create a self-contained running example. I also added a possible answer to your question, how to execute your program, following RĂșnar Bjarnason's ideas, using Scalaz 7.2. (I did not find the or operator for the natural transformations in Scalaz, so I added it here.)

I also added a few stubs to give your actions something to fiddle with and simplified your services to the handlers inside (since I had to create a new service for both languages combined). Furthermore I changed your Task.now{...} to Task{...} to create an asynchronous Task, which is executed on the last line of code.

Here is the full code:

import scala.language.{higherKinds, implicitConversions}

import scalaz._
import scalaz.concurrent.Task

/* Stubs */

case class AccruePoints()
case class AccruePointModel(cmd: AccruePoints) {
  def toEvent(code: String, description: String): PointsAccruedEvent = PointsAccruedEvent(code, description)
}
case class PointsAccruedEvent(code: String, description: String)

/* Actions */

sealed trait ServiceAction[T] extends Product with Serializable

case class ConsumeCommand(cmd: AccruePoints) extends ServiceAction[AccruePointModel]
case class CreateEvent(evt: PointsAccruedEvent) extends ServiceAction[PointsAccruedEvent]

sealed trait LogAction[T] extends Product with Serializable

case class Info(msg: String) extends LogAction[Unit]
case class Error(msg: String) extends LogAction[Unit]

/* Handlers */

object PointAccuralServiceHandler extends (ServiceAction ~> Task) {
  override def apply[A](fa: ServiceAction[A]): Task[A] = fa match {
    case ConsumeCommand(cmd) => {
      println("Service ConsumeCommand:" + cmd)
      Task(consume(cmd))
    }
    case CreateEvent(evt) => {
      println("Service CreateEvent:" + evt)
      Task(evt)
    }
  }

  def consume(cmd: AccruePoints): AccruePointModel =
    AccruePointModel(cmd)
}

case object LogServiceConsoleHandler extends (LogAction ~> Task) {
  override def apply[A](fa: LogAction[A]): Task[A] = fa match {
    case Info(m) =>
      Task(info(m))
    case Error(m) =>
      Task(error(m))
  }

  def info(msg: String): Unit = {
    println(s"INFO: $msg")
  }

  def error(msg: String): Unit = {
    println(s"ERROR: $msg")
  }
}

/* Execution */

class Service[F[_]](implicit I1: Inject[ServiceAction, F], I2: Inject[LogAction, F]) {
  def consume(cmd: AccruePoints): Free[F, AccruePointModel] = Free.liftF(I1(ConsumeCommand(cmd)))

  def emit(evt: PointsAccruedEvent): Free[F, PointsAccruedEvent] = Free.liftF(I1(CreateEvent(evt)))

  def info(msg: String): Free[F, Unit] = Free.liftF(I2(Info(msg)))

  def error(msg: String): Free[F, Unit] = Free.liftF(I2(Error(msg)))
}

object Service {
  implicit def instance[F[_]](implicit I1: Inject[ServiceAction, F], I2: Inject[LogAction, F]) = new Service[F]
}

def prg[F[_]](implicit service: Service[F]) = {
  val cmd = AccruePoints()
  for {
    _ <- service.info("Command: " + cmd)
    model <- service.consume(cmd)
    _ <- service.info("Model: " + model)
    evt <- service.emit(model.toEvent("200", "Event Sent"))
    _ <- service.info("Event:" + evt)
  } yield evt
}

type App[A] = Coproduct[ServiceAction, LogAction, A]

def or[F[_], G[_], H[_]](f: F ~> H, g: G ~> H) =
  new (({type t[x] = Coproduct[F, G, x]})#t ~> H) {
    override def apply[A](c: Coproduct[F, G, A]): H[A] = c.run match {
      case -\/(fa) => f(fa)
      case \/-(ga) => g(ga)
    }
  }

val app = prg[App]

val ret = app.foldMap(or(PointAccuralServiceHandler, LogServiceConsoleHandler))
ret.unsafePerformSync