|
| 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) |
0 commit comments