What has been your experience of using Scalaz? Have you ever tried to combine Free Monad algebra using it?
Software Engineer Afsal Thaj looks at his experience of trying this, check out his coding below.
'Here is my gist that combines the various algebra defined in various parts of the application, using Free Monad with in Scalaz! For those who are impatient, jump straight to line number 58, that handles what Scalaz is missing!
1 package com.thaj.functionalprogramming.exercises.part4
2
3 import scalaz.{ Scalaz, _ }
4 import Scalaz._
5
6 // Thanks to https://underscore.io/blog/posts/2017/03/29/free-inject.html
7 object CombineFreeInScalaz {
8 sealed trait Logging[A]
9 case class Info(s: String) extends Logging[Unit]
10
11 sealed trait BridgeExec[A]
12 case class PrintBridge(s: String) extends BridgeExec[Unit]
13
14 // This is just for describing how things work, and doesn't need to defined in scalaz.
15 trait Inject[F[_], G[_]] {
16 def inj[A](fa: F[A]): G[A]
17 }
18
19 // Inject implicits exists in scalaz, pointed out here for better understandability.
20 implicit def injectCoproductLeft[F[_], X[_]]: Inject[F, Coproduct[F, X, ?]] =
21 new Inject[F, Coproduct[F, X, ?]] {
22 def inj[A](fa: F[A]): Coproduct[F, X, A] = Coproduct.leftc(fa)
23 }
24
25 // Inject implicits exists in scalaz, pointed out here for better understandability.
26 // It is similar to the above implicit definition for left, however, we assume that the right can be `R`
27 // specifiying it could be another coproduct or in fact it could be anything, and we use recursive implicit
28 // resolution.
29 implicit def injectCoproductRight[F[_], R[_], X[_]](implicit I: Inject[F, R]): Inject[F, Coproduct[X, R, ?]] =
30 new Inject[F, Coproduct[X, R, ?]] {
31 def inj[A](fa: F[A]): Coproduct[X, R, A] = Coproduct.rightc(I.inj(fa))
32 }
33
34 // Sadly, the above definition doesn't take care of the fact that what if R =:= F
35 implicit def injectReflexive[F[_]]: Inject[F, F] =
36 new Inject[F, F] {
37 def inj[A](fa: F[A]): F[A] = fa
38 }
39
40 // A helper lift function that can lift any of your algebra to a Coproduct that defines your entire app.
41 def inject[F[_], C[_], A](fa: F[A])(implicit m: Inject[F, C]) = Free.liftF[C, A](m.inj(fa))
42
43 // However, we have individual interpreters for each component in the app.
44 // But what we need is an interpreter that interprets the coproduct to some unified effect.
45 // Copying cats or in fact turned out to be cats implementation
46 def mixInterpreters[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): Coproduct[F, G, ?] ~> H = {
47 new (Coproduct[F, G, ?] ~> H) {
48 def apply[A](fa: Coproduct[F, G, A]): H[A] = {
49 fa.run match {
50 case -\/(ff) => f(ff)
51 case \/-(gg) => g(gg)
52 }
53 }
54 }
55 }
56
57 // A helper to access mixInterpreters
58 implicit class MixInterpreterOps[F[_], H[_]](val f: F ~> H) {
59 def or[G[_]](g: G ~> H): Coproduct[F, G, ?] ~> H = mixInterpreters[F, G, H](f, g)
60 }
61
62 // Lifted first algebra to a Corproduct
63 private val logAsCoproduct: Free[Coproduct[Logging, BridgeExec, ?], Unit] =
64 inject[Logging, Coproduct[Logging, BridgeExec, ?], Unit](Info("printLogging"))
65
66 // Lifted second algebra to a Corproduct
67 private val bridgeCoproduct: Free[Coproduct[Logging, BridgeExec, ?], Unit] =
68 inject[BridgeExec, Coproduct[Logging, BridgeExec, ?], Unit](PrintBridge("pringBridge"))
69
70 // Interpreter 1
71 private def runLogging: Logging ~> Id = new (Logging ~> Id) {
72 def apply[A](fa: Logging[A]): Id[A] = fa match {
73 case Info(x) => println(x)
74 }
75 }
76
77 // Interpreter 2
78 private def runBridge: BridgeExec ~> Id = new (BridgeExec ~> Id) {
79 def apply[A](fa: BridgeExec[A]): Id[A] = fa match {
80 case PrintBridge(x) => println(x)
81 }
82 }
83
84 // App interpreter
85 val interpreter: Coproduct[Logging, BridgeExec, ?] ~> Scalaz.Id = runLogging.or(runBridge)
86
87 // Your free program that is easily testable.
88 val program: Free[Coproduct[Logging, BridgeExec, ?], Unit] =
89 for {
90 _ <- logAsCoproduct
91 _ <- bridgeCoproduct
92 } yield ()
93
94 // Call this !
95 def runThis = program.foldMap(interpreter)
96 }
Well, that was useless toy example.
I think , with my experience with Free (learning scalaz, learning the red book, reading Free things, Runar’s paper on Trampoling to Free and in fact implementing free things in production with Scala), I can say operations as data with Free is attractive, but that can bring significant set of boiler plates that is strong enough to damage the entire design of the app if you are doing something non-trivial. So you need to really consider the complexity of your app before you try to make it simple with Free and find ways to hide away boilers from business logic. A blog can’t be your source of truth as most of them including this one talks Free using it’s constructs.
Back to the tangent, what happens if we have three algebras? Well, that was kind of a terrible fight with Scala type system but that’s fine for me personally ! I will also consider the library iotaz to achieve the same with less boiler plates.
1
2
3 import scalaz.{Scalaz, _}
4 import Scalaz._
5
6 // Thanks to https://underscore.io/blog/posts/2017/03/29/free-inject.html
7 object CombineFreeInScalaz {
8 sealed trait Logging[A]
9 case class Info(s: String) extends Logging[Unit]
10
11 sealed trait BridgeExec[A]
12 case class PrintBridge(s: String) extends BridgeExec[Unit]
13
14 sealed trait BridgeExe1c[A]
15 case class PrintBridge1(s: String) extends BridgeExe1c[Unit]
16
17 // This is just for describing how things work, and doesn't need to defined in scalaz.
18 trait Inject[F[_], G[_]] {
19 def inj[A](fa: F[A]): G[A]
20 }
21
22 // Inject implicits exists in scalaz, pointed out here for better understandability.
23 implicit def injectCoproductLeft[F[_], X[_]]: Inject[F, Coproduct[F, X, ?]] =
24 new Inject[F, Coproduct[F, X, ?]] {
25 def inj[A](fa: F[A]): Coproduct[F, X, A] = Coproduct.leftc(fa)
26 }
27
28 // Inject implicits exists in scalaz, pointed out here for better understandability.
29 // It is similar to the above implicit definition for left, however, we assume that the right can be `R`
30 // specifiying it could be another coproduct or in fact it could be anything, and we use recursive implicit
31 // resolution.
32 implicit def injectCoproductRight[F[_], R[_], X[_]](implicit I: Inject[F, R]): Inject[F, Coproduct[X, R, ?]] =
33 new Inject[F, Coproduct[X, R, ?]] {
34 def inj[A](fa: F[A]): Coproduct[X, R, A] = Coproduct.rightc(I.inj(fa))
35 }
36
37 // Sadly, the above definition doesn't take care of the fact that what if R =:= F
38 implicit def injectReflexive[F[_]]: Inject[F, F] =
39 new Inject[F, F] {
40 def inj[A](fa: F[A]): F[A] = fa
41 }
42
43 // A helper lift function that can lift any of your algebra to a Coproduct that defines your entire app.
44 def inject[F[_], C[_], A](fa: F[A])(implicit m: Inject[F, C]) = Free.liftF[C, A](m.inj(fa))
45
46 // However, we have individual interpreters for each component in the app.
47 // But what we need is an interpreter that interprets the coproduct to some unified effect.
48 // Copying cats or in fact turned out to be cats implementation
49 def mixInterpreters[F[_], G[_], H[_]](f: F ~> H, g: G ~> H): Coproduct[F, G, ?] ~> H = {
50 new (Coproduct[F, G, ?] ~> H) {
51 def apply[A](fa: Coproduct[F, G, A]): H[A] = {
52 fa.run match {
53 case -\/(ff) => f(ff)
54 case \/-(gg) => g(gg)
55 }
56 }
57 }
58 }
59
60 type SubApp[A] = Coproduct[BridgeExec, BridgeExe1c, A]
61 type App[A] = Coproduct[Logging, SubApp, A]
62
63 // A helper to access mixInterpreters
64 implicit class MixInterpreterOps[F[_], H[_]](val f: F ~> H) {
65 def or[G[_]](g: G ~> H): Coproduct[F, G, ?] ~> H = mixInterpreters[F, G, H](f, g)
66 }
67
68 // Lifted first algebra to a Corproduct
69 private val logAsCoproduct: Free[ App, Unit] =
70 inject[Logging, App, Unit](Info("printLogging"))
71
72 // Lifted second algebra to a Corproduct
73 private val bridgeCoproduct: Free[App, Unit] =
74 inject[BridgeExec, App, Unit](PrintBridge("pringBridge"))
75
76 // Interpreter 1
77 private def runLogging: Logging ~> Id = new (Logging ~> Id) {
78 def apply[A](fa: Logging[A]): Id[A] = fa match {
79 case Info(x) => println(x)
80 }
81 }
82
83 // Interpreter 2
84 private def runBridge: BridgeExec ~> Id = new (BridgeExec ~> Id) {
85 def apply[A](fa: BridgeExec[A]): Id[A] = fa match {
86 case PrintBridge(x) => println(x)
87 }
88 }
89
90 private def runBridges: BridgeExe1c ~> Id = new (BridgeExe1c ~> Id) {
91 def apply[A](fa: BridgeExe1c[A]): Id[A] = fa match {
92 case PrintBridge1(x) => println(x)
93 }
94 }
95
96 val sub: ~>[Coproduct[BridgeExec, BridgeExe1c, ?], Scalaz.Id] = runBridge.or(runBridges)
97 // App interpreter
98 val interpreter: App ~> Scalaz.Id = runLogging.or[Coproduct[BridgeExec, BridgeExe1c, ?]](sub)
99
100 // Your free program that is easily testable.
101 val program: Free[App, Unit] =
102 for {
103 _ <- logAsCoproduct
104 _ <- bridgeCoproduct
105 } yield ()
106
107 // Call this !
108 def runThis = program.foldMap(interpreter)
109 }
Thanks developers ! Have fun developing only robust apps!'
This article was written by Afsal Thaj and posted originally on Medium.