Skip to content

Commit bd9d42e

Browse files
Merge pull request #1409 from bradovitt/free-monads
free monad article code
2 parents 265ebe9 + 6ec3feb commit bd9d42e

File tree

4 files changed

+244
-0
lines changed

4 files changed

+244
-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: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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] =
47+
FlatMap(this, (a: A) => Pure(f(a)))
48+
def flatMap[B: Typeable](f: A => Free[F, B]): Free[F, B] = FlatMap(this, f)
49+
50+
def foldMapAs[G[_]: Monad](using F ~> G): G[A] = this match
51+
case Pure(value) => summon[Monad[G]].pure(value)
52+
case FlatMap(sub, f) =>
53+
summon[Monad[G]]
54+
.flatMap(sub.foldMapAs[G]): in =>
55+
f(in).foldMapAs[G]
56+
case Suspend(s) => summon[F ~> G](s)
57+
58+
final case class Pure[F[_], A: Typeable](value: A) extends Free[F, A]
59+
final case class FlatMap[F[_], A: Typeable, B: Typeable](
60+
sub: Free[F, A],
61+
f: A => Free[F, B]
62+
) extends Free[F, B]
63+
final case class Suspend[F[_], A: Typeable](s: F[A]) extends Free[F, A]
64+
65+
// We define a non-monadic type:
66+
67+
trait LazyCatchable[+A]:
68+
def run(): Either[Catch, A]
69+
70+
final class Lazy[A](value: => A) extends LazyCatchable[A]:
71+
def run(): Either[Catch, A] = Try(value) match
72+
case Success(value) => Right(value)
73+
case Failure(e) => Left(Catch(e))
74+
75+
final case class Catch(e: Throwable) extends LazyCatchable[Nothing]:
76+
def run(): Either[Catch, Nothing] = Left(this)
77+
78+
// We can write monadic programs with it:
79+
80+
lazy val sumProgram: Free[LazyCatchable, Int] =
81+
for
82+
a <- Suspend(Lazy(1))
83+
b <- Suspend(Lazy(2))
84+
result <- Pure(a + b)
85+
yield result
86+
87+
// Which is translated by the compiler to this:
88+
89+
lazy val desugaredSumProgram =
90+
FlatMap(
91+
Suspend(Lazy(1)),
92+
(num1: Int) =>
93+
FlatMap(
94+
Suspend(Lazy(2)),
95+
(num2: Int) => Pure(num1 + num2)
96+
)
97+
)
98+
99+
// We provide a ~> to a Future:
100+
101+
given LazyCatchable2Future: (LazyCatchable ~> Future) with
102+
def apply[A: Typeable](f: LazyCatchable[A]): Future[A] = f match
103+
case Catch(e) => Future.failed(e)
104+
case lazyValue: Lazy[_] =>
105+
Future:
106+
lazyValue.run() match
107+
case Left(Catch(e)) => throw e
108+
case Right(value: A @unchecked) => value
109+
110+
// We define a Monad instance for Future:
111+
112+
given FutureMonad: Monad[Future] with
113+
def flatMap[A, B](fa: Future[A])(f: (A) => Future[B]): Future[B] =
114+
fa.flatMap(f)
115+
116+
def pure[A](a: A): Future[A] = Future(a)
117+
118+
override def map[A, B](fa: Future[A])(f: A => B): Future[B] = fa.map(f)
119+
120+
// We can then convert our sumProgram to a Future:
121+
122+
lazy val sumProgramFuture: Future[Int] = sumProgram.foldMapAs[Future](using
123+
FutureMonad,
124+
LazyCatchable2Future
125+
) // Future computes to 3
126+
127+
// Let's consider a more advanced workflow DSL:
128+
129+
enum WorkflowCommand:
130+
case FeelInspiredToLearn
131+
case LikeFriendlyEnvironments
132+
case WantToHelpPeopleBuildConfidenceCoding
133+
case JoinBaeldungAsAWriter
134+
135+
// We can then define our logic:
136+
137+
def command[C <: WorkflowCommand](c: => C): Free[LazyCatchable, C] = Suspend(
138+
Lazy(c)
139+
)
140+
141+
lazy val joinBaeldungWorkflow: Free[LazyCatchable, WorkflowCommand] =
142+
for
143+
_ <- command(WorkflowCommand.FeelInspiredToLearn)
144+
_ <- command(WorkflowCommand.LikeFriendlyEnvironments)
145+
_ <- command(WorkflowCommand.WantToHelpPeopleBuildConfidenceCoding)
146+
`reachOutToday!` <- Pure(WorkflowCommand.JoinBaeldungAsAWriter)
147+
yield `reachOutToday!`
148+
149+
// Then we define a translation to Future:
150+
151+
given BaeldungWorkflowInterpreter: (LazyCatchable ~> Future) with
152+
private def askQuestion(question: String, repeat: Boolean = false): Boolean =
153+
if repeat then print(s"\nInvalid response: try again (y or n) ")
154+
else print(s"\n$question (y or n) ")
155+
156+
readChar() match
157+
case 'y' | 'Y' => true
158+
case 'n' | 'N' => false
159+
case _ => askQuestion(question, true)
160+
161+
private def step[C <: WorkflowCommand](
162+
question: String,
163+
command: C,
164+
error: String
165+
): Future[C] = Future:
166+
if askQuestion(question) then command
167+
else throw new Exception(error)
168+
169+
def apply[A: Typeable](f: LazyCatchable[A]): Future[A] = f match
170+
case Catch(e) => Future.failed(e)
171+
case lazyCmd: Lazy[_] =>
172+
lazyCmd.run() match
173+
case Left(Catch(e)) => Future.failed(e)
174+
case Right(command: WorkflowCommand) =>
175+
command match
176+
case WorkflowCommand.FeelInspiredToLearn =>
177+
step(
178+
question = "Do you feel inspired to learn Scala?",
179+
command = command,
180+
error =
181+
"Baeldung has tutorials for other technologies too, like Java."
182+
)
183+
case WorkflowCommand.LikeFriendlyEnvironments =>
184+
step(
185+
question = "Do you like friendly environments?",
186+
command = command,
187+
error = "Bye."
188+
)
189+
case WorkflowCommand.WantToHelpPeopleBuildConfidenceCoding =>
190+
step(
191+
question =
192+
"Do you want to help people build confidence coding?",
193+
command = command,
194+
error = "Baeldung tutorials are reliable and informative."
195+
)
196+
case WorkflowCommand.JoinBaeldungAsAWriter =>
197+
Future.successful(command)
198+
case Right(misc) => Future.successful(misc)
199+
200+
// The translation is then very simple and intuitive:
201+
202+
lazy val joinBaeldung: Future[WorkflowCommand] = joinBaeldungWorkflow
203+
.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)