Skip to content

Instantly share code, notes, and snippets.

@igor-ramazanov
Last active September 19, 2018 11:11
Show Gist options
  • Save igor-ramazanov/3bc03a2ecd37554d8055b9ace89c7868 to your computer and use it in GitHub Desktop.
Save igor-ramazanov/3bc03a2ecd37554d8055b9ace89c7868 to your computer and use it in GitHub Desktop.
An example how to compile a higher-level language into a lower-level one with parameterised argument types in Free monads way
package io.github.themirrortruth
object KVStore {
def main(args: Array[String]): Unit = {
import cats.free.Free
import cats.{Id, ~>}
//Strict language for working with abstract key-value stores defined as Algebraic Data Type (ADT)
sealed trait KVStoreApi[Key, Value, Result]
extends Product
with Serializable
//Encodes 'get by key' operation into the case class
final case class Get[Key, Value](key: Key)
extends KVStoreApi[Key, Value, Option[Value]]
//Domain model
final case class User(username: String, password: String)
//Second, higher level language than 'KVStoreApi' that can be compiled into it
sealed trait UserApi[Result] extends Product with Serializable
//Encodes 'get user by username and password' into the case class
final case class GetUser(username: String, password: String)
extends UserApi[Option[User]]
//lifts the 'Get by key' operation into Free Monad context, so we will be able to use 'for comprehension' syntax
def get[Key, Value](
key: Key): Free[KVStoreApi[Key, Value, ?], Option[Value]] =
Free.liftF(Get[Key, Value](key): KVStoreApi[Key, Value, Option[Value]])
//lifts the 'Get user by username and password' operation into Free Monad context, so we will be able to use 'for comprehension' syntax
def getUser(userName: String,
password: String): Free[UserApi, Option[User]] =
Free.liftF(GetUser(userName, password))
//UserApi to KVStoreApi interpreter, interprets operations from UserAPI ADT into the lower-level KvStoreAPI
val userApiToKvInterpreter =
new (UserApi ~> Free[KVStoreApi[String, String, ?], ?]) {
override def apply[A](
fa: UserApi[A]): Free[KVStoreApi[String, String, ?], A] = fa match {
case GetUser(username, password) =>
get[String, String](username).map {
case Some(p) if p == password => Some(User(username, p))
case _ => None
}
}
}
//Interprets operations from KVStoreAPI into the most lower-level Monad one.
//Here is 'Id' monad is used, but it can be any Monad like 'Task' from Monix, 'IO' from cats-effect, etc.
val userKvToIdInterpreter = new (KVStoreApi[String, String, ?] ~> Id) {
private val usernamesToPasswords = Map("some_user" -> "some_password")
override def apply[A](fa: KVStoreApi[String, String, A]): Id[A] = {
fa match {
case Get(key: String) => usernamesToPasswords.get(key)
}
}
}
println(
//the operation is not running yet here, we just have built a description of program that can introspected later
getUser("some_user", "some_password")
//compiling description of the program into the KVStoreAPI operations
.foldMap(userApiToKvInterpreter)
//compiling operations from KVStoreAPI into the 'Id' monad, 'Id' monad is runnning eagerly, so it will print a result.
//however, if we use something like Monix' Task, then we'd have to run it on some 'Scheduler' (analogue of ExecutionContext for Futures)
.foldMap(userKvToIdInterpreter))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment