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 " \n Invalid 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 )
0 commit comments