Last active
September 19, 2018 11:11
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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