Cats

Freestyle is built on top of Cats’ Free and FreeApplicative and FreeS and FreeS.Par are just type aliases:

import cats.free.{ Free, FreeApplicative }

object aliases {
  type FreeS[F[_], A] = Free[FreeApplicative[F, ?], A]

  object FreeS {
    type Par[F[_], A] = FreeApplicative[F, A]
  }
}

A freestyle module (with @module) is an easy way to combine multiple algebras and create a Coproduct of the underlying algebras together with the necessary Inject instances.

  • A Coproduct is a combination of data types. An operation of type type FooBarOp[A] = Coproduct[FooOp, BarOp, A] can either by a FooOp or a BarOp.
  • Inject is a type class which can inject a data type in a Coproduct containing that specific data type. An Inject[FooOp, FooBarOp] instance can inject a FooOp operation into the FooBarOp coproduct.

FreeS with Cats

As FreeS is a monad and FreeS.Par an applicative, they can be used like any other monad/applicative in Cats.

To see how we can use FreeS and FreeS.Par in combination with existing Cats functions, we will create a simple algebra with a sum and a product operation:

import freestyle._
// import freestyle._

@free trait Calc {
  def sum(a: Int, b: Int): FS[Int]
  def product(a: Int, b: Int): FS[Int]
}
// defined trait Calc
// defined object Calc

A simple Id handler:

import cats.Id
// import cats.Id

implicit val idHandler = new Calc.Handler[Id] {
  def sum(a: Int, b: Int) = a + b
  def product(a: Int, b: Int) = a * b
}
// idHandler: Calc.Handler[cats.Id] = $anon$1@1ca20d6e

A handler translating the Calc algebra to Future, which introduces a little bit of artificial latency:

import scala.concurrent.Future
// import scala.concurrent.Future

import scala.concurrent.ExecutionContext.Implicits.global
// import scala.concurrent.ExecutionContext.Implicits.global

implicit val futureHandler = new Calc.Handler[Future] {
  def sum(a: Int, b: Int) =
    Future { Thread.sleep(a * 100L); a + b }
  def product(a: Int, b: Int) =
    Future { Thread.sleep(a * 100L); a * b }
}
// futureHandler: Calc.Handler[scala.concurrent.Future] = $anon$1@6cb61103

We can use Calc#sum to create a function which increments an integer.

We have incrPar and incrSeq, where multiple incrPar calls could potentially be executed in parallel:

val incrPar: Int => FreeS.Par[Calc.Op, Int] =
  Calc[Calc.Op].sum(_, 1)
// incrPar: Int => freestyle.FreeS.Par[Calc.Op,Int] = $$Lambda$7672/758994301@3ca3a431

val incrSeq: Int => FreeS[Calc.Op, Int] =
  incrPar.andThen(_.freeS)
// incrSeq: Int => freestyle.FreeS[Calc.Op,Int] = scala.Function1$$Lambda$2788/2137694098@66b635d1

Traversing

We will use traverse (provided by Cats’ Traverse) to increment a list of integers with incrPar and incrSeq:

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

import freestyle.implicits._
// import freestyle.implicits._

val numbers = List.range(1, 10)
// numbers: List[Int] = List(1, 2, 3, 4, 5, 6, 7, 8, 9)

val traversingPar = numbers.traverse(incrPar)
// traversingPar: freestyle.FreeS.Par[Calc.Op,List[Int]] = FreeApplicative(...)

val traversingSeq = numbers.traverse(incrSeq)
// traversingSeq: freestyle.FreeS[Calc.Op,List[Int]] = Free(...)

Executing these with our Id handler:

traversingPar.interpret[Id]
// res1: cats.Id[List[Int]] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

traversingSeq.interpret[Id]
// res2: cats.Id[List[Int]] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

A simple timer method giving a rough estimate of the execution time of a piece of code:

def simpleTime[A](th: => A): A = {
  val start = System.currentTimeMillis
  val result = th
  val end = System.currentTimeMillis
  println("time: " + (end - start))
  result
}
// simpleTime: [A](th: => A)A

When we execute the increment traversals again using Future, we can observe that the parallel execution is now quicker than the sequential.

import freestyle.nondeterminism._
// import freestyle.nondeterminism._

import scala.concurrent.Await
// import scala.concurrent.Await

import scala.concurrent.duration.Duration
// import scala.concurrent.duration.Duration

val futPar = traversingPar.interpret[Future]
// futPar: scala.concurrent.Future[List[Int]] = Future(<not completed>)

simpleTime(Await.result(futPar, Duration.Inf))
// time: 1191
// res3: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

val futSeq = traversingSeq.interpret[Future]
// futSeq: scala.concurrent.Future[List[Int]] = Future(<not completed>)

simpleTime(Await.result(futSeq, Duration.Inf))
// time: 4242
// res4: List[Int] = List(2, 3, 4, 5, 6, 7, 8, 9, 10)

Applicative and Monadic behavior

We can combine applicative and monadic steps by using the Cartesian builder (|@|) inside a for comprehension:

val incrAndMultiply =
  for {
    ab <- (incrPar(5) |@| incrPar(6)).tupled
    (a, b) = ab
    c <- Calc[Calc.Op].product(a, b)
    d <- FreeS.pure(c + 1)
  } yield d
// incrAndMultiply: cats.free.Free[[β$0$]cats.free.FreeApplicative[Calc.Op,β$0$],Int] = Free(...)

incrAndMultiply.interpret[Id]
// res5: cats.Id[Int] = 43

In the first line of the for comprehension, the two applicative operations using incrPar are independent and could be executed in parallel. In the last line we are using FreeS.pure as a handy alternative for (c + 1).pure[FreeS[Calc.Op, ?]] or Applicative[FreeS[CalcOp, ?]].pure(c + 1).

To see the effect of replacing multiple independent monadic steps with one applicative step, we can again execute two similar programs using Future:

val incrSeqSum = for {
  a <- incrSeq(1)
  b <- incrSeq(2)
  c <- Calc[Calc.Op].sum(a, b)
} yield c
// incrSeqSum: cats.free.Free[[β$0$]cats.free.FreeApplicative[Calc.Op,β$0$],Int] = Free(...)

val incrParSum = for {
  ab <- (incrPar(1) |@| incrPar(2)).tupled
  c  <- Function.tupled(Calc[Calc.Op].sum _)(ab)
} yield c
// incrParSum: cats.free.Free[[β$0$]cats.free.FreeApplicative[Calc.Op,β$0$],Int] = Free(...)

val futSeq2 = incrSeqSum.interpret[Future]
// futSeq2: scala.concurrent.Future[Int] = Future(<not completed>)

simpleTime(Await.result(futSeq2, Duration.Inf))
// time: 416
// res6: Int = 5

val futPar2 = incrParSum.interpret[Future]
// futPar2: scala.concurrent.Future[Int] = Future(<not completed>)

simpleTime(Await.result(futPar2, Duration.Inf))
// time: 193
// res7: Int = 5

Cats data types

Imagine a scenario where we want to execute our simple calculations by a (remote) service that needs some configuration and our Calc algebra has no parameters taking configuration. We don’t want to add this service specific configuration to the Calc algebra because it would mean that we would also need to pass the configuration when we want to perform calculations ourselves with our Id or Future handlers. A possible solution to this dilemma is to use the Kleisli or ReaderT data type from Cats as a target for our Calc operations.

Kleisli is essentially a function where you can work with the eventual result in a for comprehension, map function; while only supplying the input to the Kleisli function when you need the final result.

Our demo configuration will only contain a number that we will add to every computation, but in a more realistic case, it could contain an API key, a URI, etc.

case class Config(n: Int)
// defined class Config

We will use Reader[Config, ?], which is a type alias for ReaderT[Id, Config, ?], as target here:

import cats.data.Reader
// import cats.data.Reader

type WithConfig[A] = Reader[Config, A]
// defined type alias WithConfig

implicit val readerHandler =
  new Calc.Handler[WithConfig] {
    def sum(a: Int, b: Int) =
      Reader { cfg => cfg.n + a + b }
    def product(a: Int, b: Int) =
      Reader { cfg => cfg.n + a * b }
  }
// readerHandler: Calc.Handler[WithConfig] = $anon$1@5bbc8d4f

With this Reader handler in place, we can translate some of the previous programs and supply the configuration to get the end result with Kleisli#run:

val configTraversing = traversingSeq.interpret[WithConfig]
// configTraversing: WithConfig[List[Int]] = Kleisli(cats.data.KleisliInstances4$$anon$3$$Lambda$5093/2053961854@a00bbde)

val configIncrSum    = incrParSum.interpret[WithConfig]
// configIncrSum: WithConfig[Int] = Kleisli(cats.data.KleisliInstances4$$anon$3$$Lambda$5093/2053961854@231176c5)

val cfg = Config(1000)
// cfg: Config = Config(1000)

configTraversing.run(cfg)
// res8: cats.Id[List[Int]] = List(1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010)

configIncrSum.run(cfg)
// res9: cats.Id[Int] = 3005

The Cats data types can be used as the target of a Freestyle program, but they also cooperate well with FreeS and FreeS.Par. In the stack example, the getCustomer method uses OptionT to combine FreeS programs.

Standing on the shoulders of giant Cats

Freestyle is only possible because of the foundational abstractions provided by Cats. Freestyle shares Cats philosophy to make functional programming in Scala simpler and more approachable to all Scala developers.

Freestyle tries to reduce the boilerplate needed to create programs using free algebras and provides ready to use effect algebras and integrations with other libraries, making it easier to work and get started with Free and consorts.