Skip to content

Commit 4c76380

Browse files
committed
free monad article code
1 parent 6f4e2b0 commit 4c76380

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed

scala-core-modules/scala-core-fp/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ This module contains articles about Scala's Functional Programming features
1414
- [Functors in Functional Programming](https://www.baeldung.com/scala/functors-functional-programming)
1515
- [Case Objects vs Enumerations in Scala](https://www.baeldung.com/scala/case-objects-vs-enumerations)
1616
- [Function Composition in Scala](https://www.baeldung.com/scala/function-composition)
17+
- [Free Monads in Scala] https://www.baeldung.com/scala/free-monads-in-scala
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
package com.baeldung.scala.freemonad
2+
3+
import scala.concurrent.{Future, ExecutionContext}
4+
import scala.util.{Try, Success, Failure}
5+
import scala.io.StdIn.readChar
6+
import scala.reflect.*
7+
8+
given ExecutionContext = ExecutionContext.Implicits.global
9+
10+
trait Monad[F[_]]:
11+
def flatMap[A, B](fa: F[A])(f: (A) => F[B]): F[B]
12+
13+
def pure[A](a: A): F[A]
14+
15+
def map[A, B](fa: F[A])(f: A => B): F[B] =
16+
flatMap(fa)(a => pure(f(a)))
17+
18+
// List composition example:
19+
20+
lazy val listComposition =
21+
for
22+
number <- 0 to 9
23+
letter <- 'A' to 'Z'
24+
yield s"$number$letter"
25+
26+
// Which the compiler transforms to:
27+
28+
lazy val desugaredListComposition =
29+
(0 to 9).flatMap: number =>
30+
('A' to 'Z').map: letter =>
31+
s"$number$letter"
32+
33+
// A functor is simpler and less powerful than a monad:
34+
35+
trait Functor[F[_]]:
36+
def map[A, B](fa: F[A])(f: A => B): F[B]
37+
38+
// A transformation between two higher-kinded types with the same type parameter:
39+
40+
trait ~>[F[_], G[_]]:
41+
def apply[A: Typeable](f: F[A]): G[A]
42+
43+
// Free allows us to lift a functor with monadic composition as a data structure:
44+
45+
sealed trait Free[F[_], A: Typeable]:
46+
def map[B: Typeable](f: A => B): Free[F, B] = FlatMap(this, (a: A) => Pure(f(a)))
47+
def flatMap[B: Typeable](f: A => Free[F, B]): Free[F, B] = FlatMap(this, f)
48+
49+
def foldMapAs[G[_]: Monad](using F ~> G): G[A] = this match
50+
case Pure(value) => summon[Monad[G]].pure(value)
51+
case FlatMap(sub, f) =>
52+
summon[Monad[G]]
53+
.flatMap(sub.foldMapAs[G]): in =>
54+
f(in).foldMapAs[G]
55+
case Suspend(s) => summon[F ~> G](s)
56+
57+
final case class Pure[F[_], A: Typeable](value: A) extends Free[F, A]
58+
final case class FlatMap[F[_], A: Typeable, B: Typeable](sub: Free[F, A], f: A => Free[F, B]) extends Free[F, B]
59+
final case class Suspend[F[_], A: Typeable](s: F[A]) extends Free[F, A]
60+
61+
// We define a non-monadic type:
62+
63+
trait LazyCatchable[+A]:
64+
def run(): Either[Catch, A]
65+
66+
final class Lazy[A](value: => A) extends LazyCatchable[A]:
67+
def run(): Either[Catch, A] = Try(value) match
68+
case Success(value) => Right(value)
69+
case Failure(e) => Left(Catch(e))
70+
71+
final case class Catch(e: Throwable) extends LazyCatchable[Nothing]:
72+
def run(): Either[Catch, Nothing] = Left(this)
73+
74+
// We can write monadic programs with it:
75+
76+
lazy val sumProgram: Free[LazyCatchable, Int] =
77+
for
78+
a <- Suspend(Lazy(1))
79+
b <- Suspend(Lazy(2))
80+
result <- Pure(a + b)
81+
yield result
82+
83+
// Which is translated by the compiler to this:
84+
85+
lazy val desugaredSumProgram =
86+
FlatMap(
87+
Suspend(Lazy(1)),
88+
(num1: Int) => FlatMap(
89+
Suspend(Lazy(2)),
90+
(num2: Int) => Pure(num1 + num2)
91+
)
92+
)
93+
94+
// We provide a ~> to a Future:
95+
96+
given LazyCatchable2Future: (LazyCatchable ~> Future) with
97+
def apply[A: Typeable](f: LazyCatchable[A]): Future[A] = f match
98+
case Catch(e) => Future.failed(e)
99+
case lazyValue: Lazy[_] => Future:
100+
lazyValue.run() match
101+
case Left(Catch(e)) => throw e
102+
case Right(value: A @unchecked) => value
103+
104+
// We define a Monad instance for Future:
105+
106+
given FutureMonad: Monad[Future] with
107+
def flatMap[A, B](fa: Future[A])(f: (A) => Future[B]): Future[B] = fa.flatMap(f)
108+
109+
def pure[A](a: A): Future[A] = Future(a)
110+
111+
override def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)
112+
113+
// We can then convert our sumProgram to a Future:
114+
115+
lazy val sumProgramFuture: Future[Int] = sumProgram.foldMapAs[Future](using FutureMonad, LazyCatchable2Future) // Future computes to 3
116+
117+
// Let's consider a more advanced workflow DSL:
118+
119+
enum WorkflowCommand:
120+
case FeelInspiredToLearn
121+
case LikeFriendlyEnvironments
122+
case WantToHelpPeopleBuildConfidenceCoding
123+
case JoinBaeldungAsAWriter
124+
125+
// We can then define our logic:
126+
127+
def command[C <: WorkflowCommand](c: => C): Free[LazyCatchable, C] = Suspend(Lazy(c))
128+
129+
lazy val joinBaeldungWorkflow: Free[LazyCatchable, WorkflowCommand] =
130+
for
131+
_ <- command(WorkflowCommand.FeelInspiredToLearn)
132+
_ <- command(WorkflowCommand.LikeFriendlyEnvironments)
133+
_ <- command(WorkflowCommand.WantToHelpPeopleBuildConfidenceCoding)
134+
`reachOutToday!` <- Pure(WorkflowCommand.JoinBaeldungAsAWriter)
135+
yield `reachOutToday!`
136+
137+
// Then we define a translation to Future:
138+
139+
given BaeldungWorkflowInterpreter: (LazyCatchable ~> Future) with
140+
private def askQuestion(question: String, repeat: Boolean = false): Boolean =
141+
if repeat then print(s"\nInvalid response: try again (y or n) ")
142+
else print(s"\n$question (y or n) ")
143+
144+
readChar() match
145+
case 'y' | 'Y' => true
146+
case 'n' | 'N' => false
147+
case _ => askQuestion(question, true)
148+
149+
private def step[C <: WorkflowCommand](question: String, command: C, error: String): Future[C] = Future:
150+
if askQuestion(question) then command
151+
else throw new Exception(error)
152+
153+
def apply[A: Typeable](f: LazyCatchable[A]): Future[A] = f match
154+
case Catch(e) => Future.failed(e)
155+
case lazyCmd: Lazy[_] => lazyCmd.run() match
156+
case Left(Catch(e)) => Future.failed(e)
157+
case Right(command: WorkflowCommand) =>
158+
command match
159+
case WorkflowCommand.FeelInspiredToLearn =>
160+
step(
161+
question = "Do you feel inspired to learn Scala?",
162+
command = command,
163+
error = "Baeldung has tutorials for other technologies too, like Java."
164+
)
165+
case WorkflowCommand.LikeFriendlyEnvironments =>
166+
step(
167+
question = "Do you like friendly environments?",
168+
command = command,
169+
error = "Bye."
170+
)
171+
case WorkflowCommand.WantToHelpPeopleBuildConfidenceCoding =>
172+
step(
173+
question = "Do you want to help people build confidence coding?",
174+
command = command,
175+
error = "Baeldung tutorials are reliable and informative."
176+
)
177+
case WorkflowCommand.JoinBaeldungAsAWriter => Future.successful(command)
178+
case Right(misc) => Future.successful(misc)
179+
180+
// The translation is then very simple and intuitive:
181+
182+
lazy val joinBaeldung: Future[WorkflowCommand] = joinBaeldungWorkflow.foldMapAs[Future](using FutureMonad, BaeldungWorkflowInterpreter)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.baeldung.scala.freemonad
2+
3+
// import scala.concurrent.Await
4+
// import scala.concurrent.duration.*
5+
6+
// @main def runWorkflow(): Unit =
7+
// val result = Await.result(joinBaeldung, 100.seconds)
8+
// println(result)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package com.baeldung.scala.freemonad
2+
3+
import com.baeldung.scala.freemonad.{*, given}
4+
5+
import scala.concurrent.Future
6+
7+
import org.scalatest.concurrent.ScalaFutures
8+
import org.scalatest.freespec.AsyncFreeSpecLike
9+
import org.scalatest.matchers.should.Matchers
10+
11+
class FreeMonadUnitTest
12+
extends AsyncFreeSpecLike
13+
with Matchers
14+
with ScalaFutures:
15+
16+
"sumProgram should be transformed as a free structure into it's value using a proper monad" in:
17+
sumProgram
18+
.foldMapAs[Future](using FutureMonad, LazyCatchable2Future)
19+
.map(_ shouldBe 3)
20+
21+
"BaeldungWorkflowInterpreter should preserve logic on non-workflow types" in:
22+
sumProgram
23+
.foldMapAs[Future](using FutureMonad, BaeldungWorkflowInterpreter)
24+
.map(_ shouldBe 3)
25+
26+
"should demonstrate for-comprehension and flatMap/map equivalence" in:
27+
listComposition shouldBe desugaredListComposition
28+
29+
"joinBaeldungWorkflows spec should succeed" in:
30+
joinBaeldungWorkflow
31+
.foldMapAs[Future](using FutureMonad, LazyCatchable2Future)
32+
.map(_ shouldBe WorkflowCommand.JoinBaeldungAsAWriter)

0 commit comments

Comments
 (0)