Skip to content

Instantly share code, notes, and snippets.

@agaro1121
Created January 9, 2018 18:29
Show Gist options
  • Save agaro1121/969040886d64e0dc1ded053341631490 to your computer and use it in GitHub Desktop.
Save agaro1121/969040886d64e0dc1ded053341631490 to your computer and use it in GitHub Desktop.
Free Monad vs Tagless Final
case class User(id: Long, name: String, age: Int)
class DatabaseError extends Throwable
case object ErrorFindingUser extends DatabaseError
case object ErrorUpdatingUser extends DatabaseError
case class ErrorDeletingUser(msg: String) extends DatabaseError
import cats.free.Free
import cats.data.{EitherK, EitherT}
import cats.{InjectK, ~>}
import cats.instances.future._
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
import DBFreeAlgebraT._
import io.github.agaro1121.taglessfinal.free.Combined.DbAndConsoleAlgebra
import io.github.agaro1121.taglessfinal.free.ConsoleFreeAlgebraT.ConsoleFreeAlgebraTI
import io.github.agaro1121.taglessfinal.{DatabaseError, ErrorDeletingUser, ErrorFindingUser, User}
sealed trait ConsoleFreeAlgebraT[T]
case class PutLine[T](t: T) extends ConsoleFreeAlgebraT[Unit]
object ConsoleFreeAlgebraT {
type ConsoleAlgebra[T] = Free[ConsoleFreeAlgebraT, T]
class ConsoleFreeAlgebraTI[F[_]](implicit I: InjectK[ConsoleFreeAlgebraT, F]) {
def putLine[T](t: T): Free[F, Unit] = Free.inject[ConsoleFreeAlgebraT, F](PutLine(t))
}
implicit def consoleFreeAlgebraTI[F[_]](implicit I: InjectK[ConsoleFreeAlgebraT, F]): ConsoleFreeAlgebraTI[F] =
new ConsoleFreeAlgebraTI[F]
val FutureInterpreter: (ConsoleFreeAlgebraT ~> Future) = new (ConsoleFreeAlgebraT ~> Future) {
override def apply[A](fa: ConsoleFreeAlgebraT[A]): Future[A] =
fa match {
case PutLine(t) =>
Future.successful(
println(t)
).asInstanceOf[Future[A]]
}
}
}
sealed trait DBFreeAlgebraT[T]
case class Create[T](t: T) extends DBFreeAlgebraT[Boolean]
case class Read[T](id: Long) extends DBFreeAlgebraT[Either[DatabaseError, T]]
case class Delete[T](id: Long) extends DBFreeAlgebraT[Either[DatabaseError, Unit]]
object DBFreeAlgebraT {
type DBFreeAlgebra[T] = Free[DBFreeAlgebraT, T]
class DBFreeAlgebraTI[F[_]](implicit I: InjectK[DBFreeAlgebraT, F]) {
def create[T](t: T): Free[F, Boolean] =
Free.inject[DBFreeAlgebraT, F](Create(t))
def read[T](id: Long): Free[F, Either[DatabaseError, User]] =
Free.inject[DBFreeAlgebraT, F](Read(id))
def delete[T](id: Long): Free[F, Either[DatabaseError, Unit]] =
Free.inject[DBFreeAlgebraT, F](Delete(id))
}
implicit def dBFreeAlgebraTI[F[_]](implicit I: InjectK[DBFreeAlgebraT, F]): DBFreeAlgebraTI[F] =
new DBFreeAlgebraTI[F]
val FutureInterpreter = new (DBFreeAlgebraT ~> Future) {
val users: mutable.Map[Long, User] = mutable.Map.empty
override def apply[A](fa: DBFreeAlgebraT[A]): Future[A] =
fa match {
case Create(user) =>
val inserted = users.put(user.asInstanceOf[User].id, user.asInstanceOf[User])
Future.successful(inserted.isEmpty || inserted.isDefined).map(_.asInstanceOf[A])
case Read(id) =>
Future.successful(users.get(id).toRight(ErrorFindingUser)).map(_.asInstanceOf[A])
case Delete(id) => {
import cats.syntax.either._
val deleted = users.remove(id)
Future.successful(
deleted.fold(ErrorDeletingUser(s"User with Id($id) was not there").asLeft[Unit])(_ => Right(()))
).map(_.asInstanceOf[A])
}
}
}
}
object Combined {
type DbAndConsoleAlgebra[T] = EitherK[DBFreeAlgebraT, ConsoleFreeAlgebraT, T]
val FutureInterpreter: DbAndConsoleAlgebra ~> Future =
DBFreeAlgebraT.FutureInterpreter or ConsoleFreeAlgebraT.FutureInterpreter
}
class FreeUserRepo(implicit
DB: DBFreeAlgebraTI[Combined.DbAndConsoleAlgebra],
C: ConsoleFreeAlgebraTI[Combined.DbAndConsoleAlgebra]) {
def getUser(id: Long): Free[DbAndConsoleAlgebra, Either[DatabaseError, User]] = DB.read(id)
def addUser(user: User): Free[DbAndConsoleAlgebra, Boolean] = DB.create(user)
type DbAndConsoleAlgebraContainer[A] = Free[DbAndConsoleAlgebra, A]
def updateUser(user: User): Free[DbAndConsoleAlgebra, Either[DatabaseError, Boolean]] = (for {
userFromDB <- EitherT(getUser(user.id))
_ <- EitherT.liftF(C.putLine(s"We found user($userFromDB)!!"))
successfullyAdded <- EitherT.liftF[DbAndConsoleAlgebraContainer, DatabaseError, Boolean](addUser(user))
} yield successfullyAdded).value
}
object DBFreeAlgebraRunner extends App {
val repo = new FreeUserRepo
println(Await.result(
(for {
_ <- repo.addUser(User(1, "Bob", 31))
dbErrorOrSuccessfullyUpdated <- repo.updateUser(User(1, "Bobby", 31))
} yield dbErrorOrSuccessfullyUpdated).foldMap(Combined.FutureInterpreter),
1 second))
}
import cats.Monad
import cats.data.EitherT
import cats.instances.future._
import scala.collection.mutable
import scala.concurrent.duration._
import scala.concurrent.{Await, Future}
import scala.concurrent.ExecutionContext.Implicits.global
trait ConsoleAlgebra[F[_]] {
def putLine[T](t: T): F[Unit]
}
object ConsoleAlgebra {
implicit object FutureInterpreter extends ConsoleAlgebra[Future] {
override def putLine[T](t: T): Future[Unit] = Future.successful{
println(t)
}
}
}
trait DatabaseAlgebra[F[_], T] {
def create(t: T): F[Boolean]
def read(id: Long): F[Either[DatabaseError, T]]
def delete(id: Long): F[Either[DatabaseError, Unit]]
}
object DatabaseAlgebra {
val FutureInterpreter: DatabaseAlgebra[Future, User] =
new DatabaseAlgebra[Future, User] {
val users: mutable.Map[Long, User] = mutable.Map.empty
override def create(user: User): Future[Boolean] = {
val inserted = users.put(user.id, user)
Future.successful(inserted.isEmpty || inserted.isDefined)
}
override def read(id: Long): Future[Either[DatabaseError, User]] =
Future.successful(users.get(id).toRight(ErrorFindingUser))
override def delete(id: Long): Future[Either[DatabaseError, Unit]] = {
import cats.syntax.either._
val deleted = users.remove(id)
Future.successful(
deleted.fold(ErrorDeletingUser(s"User with Id($id) was not there").asLeft[Unit])(_ => Right(())))
}
}
}
class UserRepo[F[_]](DB: DatabaseAlgebra[F, User],
C: ConsoleAlgebra[F])
(implicit M: Monad[F]) {
def getUser(id: Long): F[Either[DatabaseError, User]] = DB.read(id)
def addUser(user: User): F[Boolean] = DB.create(user)
def updateUser(user: User): F[Either[DatabaseError, Boolean]] = {
(for {
userFromDB <- EitherT(getUser(user.id))
_ <- EitherT.liftF(C.putLine(s"We found user($userFromDB)!!"))
successfullyAdded <- EitherT.liftF[F, DatabaseError, Boolean](addUser(user))
} yield successfullyAdded).value
}
}
object UserRepoRunner extends App {
val repo = new UserRepo(DatabaseAlgebra.FutureInterpreter, ConsoleAlgebra.FutureInterpreter)
println(Await.result(
(for {
_ <- repo.addUser(User(1, "Bob", 31))
dbErrorOrSuccessfullyUpdated <- repo.updateUser(User(1, "Bobby", 31))
} yield dbErrorOrSuccessfullyUpdated),
1 second))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment