/liberator

Liberator - sent to make you Free

Primary LanguageScalaMIT LicenseMIT

This project is EOL. Please use https://github.com/typelevel/cats-tagless.

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 or tagless algebras, without boilerplate.

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

Using Liberator

Liberator is built for Scala 2.11 and 2.12.

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

scalaVersion := "2.11.11"
libraryDependencies += "io.aecor" %% "liberator" % "0.8.0"
scalacOptions += "-Ypartial-unification"
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-M10" cross CrossVersion.full)

or

scalaVersion := "2.12.4"
libraryDependencies += "io.aecor" %% "liberator" % "0.8.0"
scalacOptions += "-Ypartial-unification"
addCompilerPlugin("org.scalameta" % "paradise" % "3.0.0-M10" cross CrossVersion.full)

Usage example

The ? syntax for type lambdas is provided by kind-projector compiler plugin.

import io.aecor.liberator.macros.free

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

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

/*
 * We need an empty companion object as a temporary workaround
 * for https://github.com/scalameta/paradise/issues/176
 */
object KeyValueStore

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 KeyValueStoreOp[A] extends Product with Serializable
  object KeyValueStoreOp {
    final case class SetValue(key: String, value: String) extends KeyValueStoreOp[Unit]
    final case class GetValue(key: String) extends KeyValueStoreOp[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)
      }
    }

  implicit def freeInstance[F[_]](implicit inject: InjectK[KeyValueStoreOp, F]): KeyValueStore[Free[F, A]] =
    fromFunctionK(new (KeyValueStoreFree ~> Free[F, A]) {
      def apply[A](op: KeyValueStoreFree[A]): Free[F, A] = Free.inject(op) 
    })
    
  implicit val freeAlgebra: Algebra.Aux[KeyValueStore, KeyValueStoreOp] =
    new Algebra[KeyValueStore] {
      type Out[A] = KeyValueStoreOp[A]
      override def apply[F[_]](of: KeyValueStore[F]): KeyValueStoreOp ~> F =
        KeyValueStore.toFunctionK(of)
    }
}

Given all above you can write your programs like this

import io.aecor.liberator.macros.free
import io.aecor.liberator.data.ProductKK
import io.aecor.liberator.Algebra

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

object Logging

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 algebra = Algebra[ProductKK[KeyValueStore, Logging, ?[_]]]

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

val freeProgram = program[Free[algebra.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

FunctorK

Liberator provides @functorK annotation.
This macros generates FunctorK instance.
Use case:

import io.aecor.liberator.macros.functorK
import io.aecor.liberator.syntax._

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

val fLogging: Logging[F] = ...
val f2g: F ~> G = ...
val gLogging: Logging[G] = fLogging.mapK(f2g) 

ReifiedInvocations

Liberator provides @reifyInvocations annotation. This macros generates ReifiedInvocations instance. Use case:

import io.aecor.liberator.macros.reifyInvocations
import io.aecor.liberator.syntax._
import monix.eval.Task._

@reifyInvocations
trait KVS[F[_]] {
  def set(k: String, v: String): F[Unit]
  def get(k: String): F[Option[Unit]]
}

type Halt[F[_], A] = F[Unit]

object LoggingKVS extends KVS[Halt[Task, ?]] {
  def set(k: String, v: String): Task[Unit] = Task(println(s"Set $k to $v"))
  def get(k: String): Task[Unit] = Task(println(s"Get $k to $v"))
}

val realKVS: KVS[Task] = ...

val introspected = ReifiedInvocations[KVS].mapK {
  new (Invocation[KVS, ?] ~> Task) {
    def apply[A](invocation: Invocation[KVS, A]): Task[A] =
      invocation.invoke(LoggingKVS).flatMap(_ => invocation.invoke(realKVS))
  }
}

Known issues

  • There is a possibility of type name collision if base trait contains abstract type named F and it is not last unary type constructor, e.g. trait Foo[F, A, B[_]]