free monads in business application

What

This post describes Letter Shop API implementation which uses Free Monads, exactly Free from Cats

tl;dr

show me the code

Why

There are some nice materials about free monads on the net, but I always wanted to see how such concepts, applied to regular business applications, look like. How difficult or cumbersome it is to implement some business use case using a particular concept, e.g. free monads.

Letter Shop is my way to make business application implementation comparable. I have explained what it is in this post

How

Free monads separate algorithm definition from its implementation. We need a program - the code that describes steps of an algorithm, the compiler - the code that interprets/runs some other code for algorithm steps and data on which our algorithm works.

ADT as your internal API

We have to define all operations that are possible from business logic point of view as ADT.

ADT in Scala is described as a trait and case classes/objects implementing it. Each class/object describes one operation. Let’s look at GetCart in StorageADT:

case class GetCart(cartId: String) extends StorageADT[Cart]

GetCart case class represents one operation (getting cart of course). It takes one parameter (cartId) and returns cart, which is represented by StotrageADT parameterized with Cart type.

The whole ADT has a generic parameter which is specified in each class/object implementing it - this parameter is for return type of each operation.

Defining ADT in the first place looks like some huge amount of code that has to be written just to make free monads work, but in the end it occurs that it makes you think which operations are really needed in your code and it also defines your api. All this operations are your internal api.

You can of course have different ADTs for different parts of application, like: storage, users, validation, etc.

Helpers aka boilerplate

After defining our plain old ADT we need to lift each class/object from ADT to Free. In the easiest example it could be done simply by Free.liftF, but this works when we have only one ADT in our application. I doubt it’s true for any business application, so if we don’t write small and simple dsl we need to complicate it a little bit.

There is a special method in Free called inject, which in fact calls Free.liftF. It doesn’t lift our ADT, but it lifts value of type, which is injected as defined in a call to Free.inject. Let’s see how does it look in code:

 class StorageConstructors[F[_]](implicit I: Inject[StorageADT, F]) {
    def getCart(cartId: String): Free[F, Cart] =
      Free.inject[StorageADT, F](GetCart(cartId))
 }

getCart is called a smart constructor. This is something that makes our case class GetCart usable in context of Free when we have more than one ADT. Return type is simply Free with F and return type of given operation - Cart in the above example.

So coming back to lifting, it “repacks” (injects) from StorageADT type into F type and the result is lifted into Free. This F type in our case is LetterShop, which is Coproduct of our ADTs. I will come to Coproduct in a while.

StorageConstructors companion object defines implicit conversion from Inject to our StorageConstructors, but at the same time Inject parameter is automatically created on demand (look into Programs trait, each program needs implicit StorageConstructors).

We have also defined type alias

type LetterShop[A] = Coproduct[StorageADT, PriceADT, A]

LetterShop is a Coproduct of ADTs that we have used in application, that is StorageADT and PriceADT. Coproduct is another type from Cats which allows us to combine two ADTs and then use it with Free. If we have more than two ADTs we need to combine one coproduct of two ADTs into another coproduct with third ADT and so on.

There is a lot of code that has to be written to satisfy the compiler here and it’s only preparation for writing programs and compilers - the core part of our application.

UC’s as programs

Free is using terms like programs and compilers to talk about code that implements in abstract way the algorithm and code that really executes it. Use case (user story, name it as you wish) is a part of business logic in our code that gathers all the operations needed to achieve some goal. Typically it’s equivalent of one request from frontend to our backend code. Let’s see how does it look in code

def getCartProgram(cartId: String)(
    implicit S: StorageConstructors[LetterShop]): Free[LetterShop, Cart] = {
  import S._
  for {
    cart <- getCart(cartId)
  } yield cart
}

getCartProgram takes all needed params in the first parameter list (cartId in our case) and implicit StorageConstructors[LetterShop]. Compiler sees the need for StorageConstructors and finds implicit conversion in StorageConstructors companion object, which in turn needs Inject type. We don’t have such value in our code, but cats.free.Inject.leftInjectInstance can be found. That’s why everything compiles and all the types are in place.

All steps of the program are coded using for comprehension. Return type for each program is also Free. That makes it perfect for composition, because all our business logic returns Free. Let’s look at checkoutCartProgram:

def checkoutCartProgram(cartId: String, promoCode: Option[String])(
    implicit S: StorageConstructors[LetterShop],
    P: PromoConstructors[LetterShop]): Free[LetterShop, Checkout] = {
  import S._, P._
  val uuid = UUID.randomUUID.toString
  for {
    p    <- checkCartProgram(cartId, promoCode)
    cart <- getCart(cartId)
    _    <- addToReceipts(cartId, ReceiptHistory(p.price, uuid, cart.letters))
    _    <- removeCart(cartId)
  } yield Checkout(p.price, uuid)
}

This UC/program has more steps and as we can see first step just calls other program. That is followed by some other steps. Of course such composition is also possible without using Free, but having one type container for every UC makes it a lot easier.

Program only generates steps of the algorithm. When it comes to the execution … we need another code - compiler.

Compilers

We always wanted to write applications in which parts of them can be changed to some other parts without fixing the whole application. There are many ways to do this (interfaces, ports and adapters architecture, etc.). Free monads give us one more option. It’s nice because it’s really interchangeable without bothering that our business logic will fail (at least from business logic “algorithm” point of view).

So let’s write a compiler. In case of free monads from Cats compiler is a cats.arrow.NaturalTransformation (aliased as ~>) from ADT, that we have created, to some other type. It could be Future, but the simplest one is Id. Compiler has to be written for defined ADTs.

def storageCompiler: StorageADT ~> Id =
  new (StorageADT ~> Id) {
  ... 
  def apply[A](fa: StorageADT[A]): Id[A] = fa match {
    case GetCart(cartId) => Cart(carts.getOrElse(cartId, ""))
    case AddToCart(cartId, letters) =>
      carts += (cartId -> letters)
      ()
  ...
  }
}

The whole code is here. This example is using TrieMap and stores everything in memory just for simplicity. It could also go to database or connect to some service - that depends on compiler implementation.

storageCompiler is a compiler for StorageADT and it defines natural transformation from StorageADT to Id. To make it work we have to implement apply method which should know what to do with every element of our ADT. That’s why we use simple pattern matching. Of course compiler will tell us if we forget to implement one of case classes/objects as they are under sealed trait.

The other compiler from our example, priceCompiler, delegates execution to PromotionService.

The last step is to combine two compilers to be able to run all our programs - it’s simple:

def compiler: LetterShop ~> Id = storageCompiler or priceCompiler

I use this compiler in Routes to be able to run programs.

lazy val getCart = get {
  path(Segment) { cartId =>
    complete(getCartProgram(cartId).foldMap(cmp))
  }
}

To run the program we need to call the program with needed params and foldMap it with chosen compiler.

Materials

The best material on practical usage of Free Monads in my opinion is Free description from Typelevel

Summary

This blog post describes how to use Free from Cats to implement exemplary business application. Of course it’s not a silver bullet to take and use in every application, but it looks interesting. Especially when flexibility in choosing the way our code is executed is needed. It also gives our code some structure with one main type to which business logic is implemented. It’s useful in many ways not to have a code in which every UC returns different type (i.e. Option, Future, Either, etc.). For sure a lot of additional code has to be written to achieve this. The decision is yours.

Comments

If you have some opinions about Free and/or this text feel free to make PR, create issues and comment on twitter.