/liberator

Liberator - sent to make you Free

Primary LanguageScalaMIT LicenseMIT

Liberator - sent to make you Free

Build Status Maven Central Join the chat at https://gitter.im/aecor/liberator

The goal of this library is to generate everything you need to create programs using Free monad, without boilerplate.

It is built using scala.meta, Cats and a bit of Shapeless.

Using Liberator

Liberator supports only Scala 2.11 due to missing support of 2.12 from scala.meta paradise (subject to change very soon)

To start using Liberator add the following to your build.sbt file:

scalaOrganization := "org.typelevel"
libraryDependencies += "io.aecor" %% "liberator" % "0.1.0"
scalacOptions += "-Ypartial-unification"
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-beta4" cross CrossVersion.full)

Usage example

@free
trait KeyValueStore[F[_]] {
  def setValue(key: String, value: String): F[Unit]

  def getValue(key: String): F[Option[String]]
}

The code above will be expanded at compile time to this (desanitized for brevity):

trait KeyValueStore[F[_]] {
  def setValue(key: String, value: String): F[Unit]
  def getValue(key: String): F[Option[String]]
}
object KeyValueStore {
  // A helper method to get an instance of KeyValueStore[F]
  def apply[F[_]](implicit instance: KeyValueStore[F]): KeyValueStore[F] = instance
  
  
  // A free AST
  sealed abstract class KeyValueStoreFree[A] extends Product with Serializable
  object KeyValueStoreFree {
    final case class SetValue(key: String, value: String) extends KeyValueStoreFree[Unit]
    final case class GetValue(key: String) extends KeyValueStoreFree[Option[String]]
  }
  
  // A function to convert a natural transformation to your trait
  def fromFunctionK[F[_]](f: KeyValueStoreFree ~> F]): KeyValueStore[F] = 
    new KeyValueStore[F] {
      def setValue(key: String, value: String): F[Unit] = 
        f(KeyValueStoreFree.SetValue(key, value))
        
      def getValue(key: String): F[Option[String]] = 
        f(KeyValueStoreFree.GetValue(key))
    }
    
    
  // A function to create a natural tranformation from your trait
  def toFunctionK[F[_]](ops: KeyValueStore[F]): KeyValueStoreFree ~> F = 
    new (KeyValueStoreFree ~> F) {
      def apply[A](op: KeyValueStoreFree[A]): F[A] = op match {
        case KeyValueStoreFree.SetValue(key, value) => ops.setValue(key, value)
        case KeyValueStoreFree.GetValue(key) => ops.getValue(key)
      }
    }
    
  type FreeHelper1[F[_]] = { type Out[A] = Free[F, A] } //workaround of a bug in scala.meta
  implicit def freeInstance[F[_]](implicit inject: Inject[KeyValueStoreFree, F]): KeyValueStore[FreeHelper1[F]#Out] = 
    fromFunctionK(new (KeyValueStoreFree ~> FreeHelper1[F]#Out) { 
      def apply[A](op: KeyValueStoreFree[A]): Free[F, A] = Free.inject(op) 
    })
    
  implicit val freeAlgebra: FreeAlgebra.Aux[KeyValueStore, KeyValueStoreFree] = 
    new FreeAlgebra[KeyValueStore] {
      type Out[A] = KeyValueStoreFree[A]
      override def apply[F[_]](of: KeyValueStore[F]): KeyValueStoreFree ~> F = 
        KeyValueStore.toFunctionK(of)
    }
}

Given all above you can write your programs like this

@free
trait Logging[F[_]] {
  def debug(s: String): F[Unit]
}

def program[F[_]: Monad: KeyValueStore: Logging](key: String): F[String] =
    for {
      value <- KeyValueStore[F].getValue(key)
      _ <- Logging[F].debug(s"Got value $value")
      newValue = UUID.randomUUID().toString
      _ <- KeyValueStore[F].setValue(key, newValue)
      _ <- Logging[F].debug(s"Update value to $newValue")
    } yield newValue    

val freeAlgebra = FreeAlgebra[ProductKK[KeyValueStore, Logging, ?[_]]]

// Notice that you don't have to know anything about presence of AST

val freeProgram = program[Free[freeAlgebra.Out, ?]]("key")

val taskKeyValueStore: KeyValueStore[Task] = ???

val taskLogging: Logging[Task] = ???

val task = freeProgram.foldMap(freeAlgebra(ProductKK(taskKeyValueStore, taskLogging)))

task.runAsync // the only side-effecting call

Known issues

  • Only supports liberation of traits of kind * -> *