In the following example, we will see how a module with two algebras can be used in combination with some of the effect algebras provided by Freestyle.

In this example, a company selling multiple varieties of apples wants to process its customers’ orders. The orders need to be validated, the stock levels of apples checked to make sure the order can be fulfilled, and finally, the order needs to be processed.

We start by importing Freestyle’s core:

import freestyle._
import freestyle.implicits._

Now we can create data types for a customer and an order together with a Config data type which holds a set of all the apple varieties the company sells.

import java.util.UUID

type CustomerId = UUID

case class Customer(id: CustomerId, name: String)
case class Order(crates: Int, variety: String, customerId: CustomerId)

case class Config(varieties: Set[String])

It should be possible to get a Customer for a specified CustomerId from a data store holding the company’s customers. We should be able to check how many crates of a specific apple variety the company has in stock, and finally, process the order.

We will represent these capabilities using two algebras: CustomerPersistence and StockPersistence.

object algebras {
  @free trait CustomerPersistence {
    def getCustomer(id: CustomerId): FS[Option[Customer]]

  @free trait StockPersistence {
    def checkQuantityAvailable(variety: String): FS[Int]
    def registerOrder(order: Order): FS[Unit]
// defined object algebras

These two algebras will be combined together in a Persistence module, that in turn will be part of our App module, that additionally includes the capabilities to fail, to read from our Config configuration, and to cache Customers using respectively error and reader from the freestyle-effects module and cache from the freestyle-cache module.

import freestyle.effects.error._
// import freestyle.effects.error._

import freestyle.effects.reader
// import freestyle.effects.reader

import freestyle.cache.KeyValueProvider
// import freestyle.cache.KeyValueProvider

object modules {
  val rd = reader[Config]
  val cacheP = new KeyValueProvider[CustomerId, Customer]

  @module trait Persistence {
    val customer: algebras.CustomerPersistence
    val stock: algebras.StockPersistence

  @module trait App {
    val persistence: Persistence

    val errorM: ErrorM
    val cacheM: cacheP.CacheM
    val readerM: rd.ReaderM
// defined object modules

To validate the order, we check to make sure the number of crates ordered is larger than zero, and that the requested variety of apple is sold by the company.

For the second part, we use the reader effect algebra to read the set of apple varieties from our Config.

We are using Validated here to combine multiple errors in a NonEmptyList represented by the type alias Validated provides an easy way to accumate errors. More information can be found on the Cats website. NonEmptyList is a List with at least one element.

The |+| syntax used below is provided by Cats (because Validated has a Semigroup instance) which makes it possible to combine the error messages of the two checks:

import modules._
// import modules._

import{NonEmptyList, ValidatedNel}
// import{NonEmptyList, ValidatedNel}

import cats.implicits._
// import cats.implicits._

def validateOrder[F[_]](order: Order, customer: Customer)(implicit app: App[F]): FreeS.Par[F, ValidatedNel[String, Unit]] =
  app.readerM.reader { config =>
    val v = ().validNel[String]
      "Number of crates ordered should be bigger than zero"))(
      _ => order.crates > 0) |+|
      "Apple variety is not available"))(
      _ => config.varieties.contains(order.variety.toLowerCase))
// validateOrder: [F[_]](order: Order, customer: Customer)(implicit app: modules.App[F])freestyle.FreeS.Par[F,[String,Unit]]

When validating an order, we need to acquire the customer’s information. If a customer places multiple orders, we don’t want to send a database request every time, so we can use the freestyle-cache module to cache customers.

In the getCustomer function, we first try to locate the customer in the cache by using the cache effect algebra, if we cannot find them, we fall back to retrieving the customer from a database (or other persistence store).

We are using the OptionT monad transformer here. With Freestyle, you generally don’t have to deal with monad transformers in your actual domain code. They are only used in the handlers and when executing your program on the edge. In this case though, Freestyle and the OptionT monad transformer work flawlessly together. OptionT is used as a convenient wrapper over a FreeS[F, Option[A]] value and the OptionT#orElseF method makes it is easy to specify what needs to happen if the inner Option is None.

// import

def getCustomer[F[_]](id: CustomerId)(implicit app: App[F]): FreeS[F, Option[Customer]] =
  // first try to get the customer from the cache
  OptionT(app.cacheM.get(id).freeS).orElseF {
    // otherwise fallback and get the customer from a persistent store
    for {
      customer <- app.persistence.customer.getCustomer(id).freeS
      _        <- customer.fold(
                    ().pure[FreeS[F, ?]])(
                    // put customer in cache
                    cust => app.cacheM.put(id, cust))
    } yield customer
// getCustomer: [F[_]](id: CustomerId)(implicit app: modules.App[F])freestyle.FreeS[F,Option[Customer]]

Next we will create a couple of domain specific exception case classes:

sealed abstract class AppleException(val message: String) extends Exception(message)
case class CustomerNotFound(id: CustomerId)               extends AppleException(s"Customer $id can not be found")
case class QuantityNotAvailable(error: String)            extends AppleException(error)
case class ValidationFailed(errors: NonEmptyList[String]) extends AppleException(errors.intercalate("\n"))

We can use the validateOrder and getCustomer methods in combination with our persistence algebras and the error effect algebra, to create the processOrder method tying everything together:

def processOrder[F[_]](order: Order)(implicit app: App[F]): FreeS[F, String] = {
  import app.persistence._, app.errorM._
  for {
    customerOpt <- getCustomer[F](order.customerId)
    customer    <- either(customerOpt.toRight(CustomerNotFound(order.customerId)))
    validation  <- validateOrder[F](order, customer)
    _           <- either(validation.toEither.leftMap(ValidationFailed))
    nbAvailable <- stock.checkQuantityAvailable(order.variety)
    _           <- either(
                       order.crates <= nbAvailable,
                         s"""There are insufficient crates of ${order.variety} apples in stock
                            |(only $nbAvailable available, while ${order.crates} needed - $order).""".stripMargin)
    _          <- stock.registerOrder(order)
  } yield s"Order registered for customer ${order.customerId}"
// processOrder: [F[_]](order: Order)(implicit app: modules.App[F])freestyle.FreeS[F,String]

As a target type of our computation, we choose a combination of Kleisli and fs2.Task.

We use Kleisli to fulfil the ReaderM constraint and Task the ErrorM constraint:

// import

import _root_.fs2.Task
// import _root_.fs2.Task

import _root_.fs2.interop.cats._
// import _root_.fs2.interop.cats._

type Stack[A] = Kleisli[Task, Config, A]
// defined type alias Stack

Now we create the interpreters or handlers for algebras of our Persistence module by implementing their specific Handler traits as x.Handler[Stack]:

import algebras._
// import algebras._

val customerId1 = UUID.fromString("00000000-0000-0000-0000-000000000000")
// customerId1: java.util.UUID = 00000000-0000-0000-0000-000000000000

implicit val customerPersistencteHandler: CustomerPersistence.Handler[Stack] =
  new CustomerPersistence.Handler[Stack] {
    val customers: Map[CustomerId, Customer] =
      Map(customerId1 -> Customer(customerId1, "Apple Juice Ltd"))
    def getCustomer(id: CustomerId): Stack[Option[Customer]] =
      Kleisli(_ =>
// customerPersistencteHandler: algebras.CustomerPersistence.Handler[Stack] = $anon$1@5458da3f

implicit val stockPersistencteHandler: StockPersistence.Handler[Stack] =
  new StockPersistence.Handler[Stack] {
    def checkQuantityAvailable(variety: String): Stack[Int] =
      Kleisli(_ => match {
          case "granny smith" => 150
          case "jonagold"     => 200
          case _              => 25

    def registerOrder(order: Order): Stack[Unit] =
      Kleisli(_ => Task.delay(println(s"Register $order")))
// stockPersistencteHandler: algebras.StockPersistence.Handler[Stack] = $anon$1@53d8d845

A handler that can cache Customers can be created with a ConcurrentHashMapWrapper and a natural transformation to our Stack type:

import cats.{~>, Id}
// import cats.{$tilde$greater, Id}

import freestyle.cache.hashmap._
// import freestyle.cache.hashmap._

import freestyle.cache.KeyValueMap
// import freestyle.cache.KeyValueMap

implicit val freestyleHasherCustomerId: Hasher[CustomerId] =
// freestyleHasherCustomerId: freestyle.cache.hashmap.Hasher[CustomerId] = freestyle.cache.hashmap.Hasher$$anon$1@7b778d09

implicit val cacheHandler: cacheP.CacheM.Handler[Stack] = {
  val rawMap: KeyValueMap[Id, CustomerId, Customer] =
    new ConcurrentHashMapWrapper[Id, CustomerId, Customer]

  val cacheIdToStack: Id ~> Stack =
    new (Id ~> Stack) {
      def apply[A](a: Id[A]): Stack[A] = a.pure[Stack]

  cacheP.implicits.cacheHandler(rawMap, cacheIdToStack)
// cacheHandler: modules.cacheP.CacheM.Handler[Stack] = freestyle.cache.KeyValueProvider$Implicits$CacheHandler@38095a35

We can now create a Freestyle program by specifying the type in processOrder as App.Op:

val program: FreeS[App.Op, String] =
  processOrder[App.Op](Order(50, "granny smith", customerId1))
// program: freestyle.FreeS[modules.App.Op,String] = Free(...)

With the persistence algebras, interpreters, and the Customer cache interpreter in place and with the right imports for the reader and error effect, we can execute a program resulting in Stack[String]:

import freestyle.effects.error.implicits._
// import freestyle.effects.error.implicits._

import rd.implicits._
// import rd.implicits._

val varieties = Set("granny smith", "jonagold", "boskoop")
// varieties: scala.collection.immutable.Set[String] = Set(granny smith, jonagold, boskoop)

val config = Config(varieties)
// config: Config = Config(Set(granny smith, jonagold, boskoop))

val result: Stack[String] = program.interpret[Stack]
// result: Stack[String] = Kleisli($$Lambda$5096/426620612@35dda37d)

We can run our Stack by supplying a Config value and running the Task.

By supplying a Config value we can run our Stack which results in a Task[String]. When we want to actually execute the program, we can run the Task by using one of the methods on Task:

val task: Task[String] =
// task: fs2.Task[String] = Task

task.unsafeRunSync.fold(println, println)
// Register Order(50,granny smith,00000000-0000-0000-0000-000000000000)
// Order registered for customer 00000000-0000-0000-0000-000000000000