Skip to content

Instantly share code, notes, and snippets.

@afsalthaj
Last active September 17, 2018 02:24
Show Gist options
  • Save afsalthaj/eeff5c7dc6d467618886f58deb17697f to your computer and use it in GitHub Desktop.
Save afsalthaj/eeff5c7dc6d467618886f58deb17697f to your computer and use it in GitHub Desktop.
import cats.free.Free
import cats.{ Monad, ~>}
import iota._
import TListK.:::
import cats.implicits._
// A monkey patching that was possible with Free Monad, where logging is
// never a part of the algebra/interface/interpreter.
// Unlike dozens of blogs and patterns, here Log is never a part of the coproduct of algebras
// (which can clutter the business-logic/monadic-for-comprehension).
object TestLogging extends App {
sealed trait Alg1[A]
object Alg1 {
final case class CreateStorage(x: String) extends Alg1[String]
final case class DeleteStorage(x: String) extends Alg1[String]
def createStorage(x: String): Free[Alg1, String] = CreateStorage(x).asAction
def deleteStorage(x: String): Free[Alg1, String] = DeleteStorage(x).asAction
}
sealed trait Alg2[A]
object Alg2 {
final case class CreateSql(x: String) extends Alg2[String]
final case class DeleteSql(x: String) extends Alg2[String]
def createSql(x: String): Free[Alg2, String] = CreateSql(x).asAction
def deleteSql(x: String): Free[Alg2, String] = DeleteSql(x).asAction
}
implicit val intAlg1: Alg1 ~> cats.Id = new (Alg1 ~> cats.Id) {
override def apply[A](fa: Alg1[A]): cats.Id[A] = fa match {
case CreateStorage(x) => s"created storage${x}"
case DeleteStorage(x) => s"deleted storage${x}"
}
}
implicit val intAlg2: Alg2 ~> cats.Id = new (Alg2 ~> cats.Id) {
override def apply[A](fa: Alg2[A]): cats.Id[A] = fa match {
case CreateSql(x) => s"created sql${x}"
case DeleteSql(x) => s"deleted sql${x}"
}
}
// This is our main program. It is a coproduct of all algebras.
type Program[A] = CopK[Alg1 ::: Alg2 ::: TNilK, A]
// The program being lifted to Free.
type FreeProgram[A] = Free[Program, A]
// Given Alg1 ~> F, Alg2 ~> F, we get Program ~> F, and that will be our interpeter
object Program {
def interpreter[F[_]](
implicit M: Monad[F],
I1: Alg1 ~> F,
I2: Alg2 ~> F,
): Program ~> F = CopK.FunctionK.summon
}
// Forget about this. It is Free monad usual boiler plate
def lift[A[_], B[α] <: iota.CopK[_, α]](
implicit I: CopK.Inject[A, B]
): A ~> Free[B, ?] =
new (A ~> Free[B, ?]) {
override def apply[α](fa: A[α]): Free[B, α] =
Free.inject[A, B](fa)
}
// It says Free[Alg, A] can be coverted to Free[Program, A]. Usual free stuff :O
implicit class FreeOps[F[_], A](x: Free[F, A]) {
def liftF(implicit I: CopK.Inject[F, Program]): Free[Program, A] = x.foldMap(lift[F, Program])
}
// Here goes the main stuff.
import Alg1._
import Alg2._
def program(x: String): Free[Program, String] =
for {
a <- createSql(x).liftF
b <- deleteSql(x).liftF
c <- createStorage(x).liftF
d <- deleteStorage(x).liftF
} yield a + b + c + d
// A program without logging, and that's it.
println(program("afsal").foldMap(Program.interpreter[cats.Id]))
// [info] created sqlafsaldeleted sqlafsalcreated storageafsaldeleted storageafsal
// Let's add logging. let's don't touch interpreters, or the above for comprehension.
// Let's have separate logging module. Given a Program ~> F (which we already have) and a Logging ~> F where
// Logging is another algebra, then we can get Program ~> F that includes logging!
import Logging._
def logModule[F[_] : Monad](x: Program ~> F, log: Logging ~> F): Program ~> F = new (Program ~> F) {
val StorageApp= CopK.Inject[Alg1, Program]
val SqlApp = CopK.Inject[Alg2, Program]
// We can be more lazy-developer here. Just do
// override def apply[A](fa: Program[A]): F[A] = log(info(fa.toString)) >> x(fa), where toString is from iota.
override def apply[A](fa: Program[A]): F[A] = fa match {
case StorageApp(alg) => alg match {
case CreateStorage(st) => log(info(s"Trying to create a storage $st")) *> x(fa)
case DeleteStorage(st) => log(info(s"Trying to delete a storage $st")) *> x(fa)
}
case SqlApp(alg) => alg match {
case CreateSql(sq) => log(info(s"Trying to create a sql $sq")) *> x(fa)
// I don't want to log other operations.
case _ => x(fa)
}
}
}
val withLog: ~>[Program, cats.Id] = logModule[cats.Id](Program.interpreter[cats.Id], Logging.logg)
// Same prgogram with oogging
println(program("afsal").foldMap(withLog))
// [info] Trying to create a sql afsal
// [info] Trying to create a storage afsal
// [info] Trying to delete a storage afsal
// [info] created sqlafsaldeleted sqlafsalcreated storageafsaldeleted storageafsal
//
//
}
// The Logger algebra like this:
import cats.{Applicative, Eval, Later, ~>}
sealed trait Logging[A]
object Logging {
final case class Debug(msg: Eval[String]) extends Logging[Unit]
final case class Info(msg: Eval[String]) extends Logging[Unit]
final case class Warn(msg: Eval[String]) extends Logging[Unit]
final case class Error(msg: Eval[String], throwable: Eval[Option[Throwable]]) extends Logging[Unit]
// Better use smart constructors and avoid digging into the guts of log data structure.
def debug(msg: => String): Logging[Unit] = Debug(Later(msg))
def info(msg: => String): Logging[Unit] = Info(Later(msg))
def warn(msg: => String): Logging[Unit] = Warn(Later(msg))
def error(msg: => String, throwable: => Option[Throwable]): Logging[Unit] =
Error(Later(msg), Later(throwable))
// My naive Logging
val logg = new (Logging ~> cats.Id){
override def apply[A](fa: Logging[A]): cats.Id[A] = fa match {
case Debug(msg) => println(msg.value)
case Info(msg) => println(msg.value)
case Warn(msg) => println(msg.value)
case Error(msg, throwable) => println(msg.value)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment