|
| 1 | +package ch09 |
| 2 | + |
| 3 | +import cats.{Eval, MonadError} |
| 4 | +import cats.data.{Reader, Writer, State} |
| 5 | +import cats.syntax.applicative.catsSyntaxApplicativeId // pure |
| 6 | +import cats.syntax.writer.catsSyntaxWriterId // tell |
| 7 | +import cats.syntax.apply.catsSyntaxApplyOps // *> |
| 8 | + |
| 9 | +object Lib: |
| 10 | + /* |
| 11 | + 9.5.4 Exercise: Abstracting |
| 12 | + Implement a method validateAdult with the following signature |
| 13 | + */ |
| 14 | + // type MonadThrow[F[_]] = MonadError[F, Throwable] |
| 15 | + // def validateAdult[F[_] : MonadThrow as me](age: Int): F[Int] |
| 16 | + |
| 17 | + // def validateAdult[F[_] : ([G[_]] =>> MonadError[G, Throwable]) as me](age: Int): F[Int] = |
| 18 | + def validateAdult[F[_]](age: Int)(using me: MonadError[F, Throwable]): F[Int] = |
| 19 | + me.ensure(me.pure(age))(IllegalArgumentException("Age must be greater than or equal to 18"))(_ >= 18) |
| 20 | + |
| 21 | + /* |
| 22 | + 9.6.5 Exercise: Safer Folding using Eval |
| 23 | + The naive implementation of foldRight below is not stack safe. Make it so using Eval. |
| 24 | + */ |
| 25 | + def foldRight[A, B](as: List[A], acc: B)(fn: (A, B) => B): B = |
| 26 | + def foldR(xs: List[A]): Eval[B] = |
| 27 | + xs match |
| 28 | + case head :: tail => Eval.defer(foldR(tail).map(fn(head, _))) |
| 29 | + case Nil => Eval.now(acc) |
| 30 | + |
| 31 | + foldR(as).value |
| 32 | + |
| 33 | + /* |
| 34 | + 9.7.3 Exercise: Show Your Working |
| 35 | +
|
| 36 | + Rewrite factorial so it captures the log messages in a Writer. |
| 37 | + Demonstrate that this allows us to reliably separate the logs for concurrent computations. |
| 38 | + */ |
| 39 | + def slowly[A](body: => A): A = |
| 40 | + try body |
| 41 | + finally Thread.sleep(100) |
| 42 | + |
| 43 | + type Logged[A] = Writer[Vector[String], A] |
| 44 | + |
| 45 | + def factorial(n: Int): Logged[Int] = |
| 46 | + for |
| 47 | + ans <- |
| 48 | + if (n == 0) |
| 49 | + then 1.pure[Logged] |
| 50 | + else slowly(factorial(n - 1).map(_ * n)) |
| 51 | + // The log in a Writer is preserved when we map or flatMap over it. |
| 52 | + _ <- Vector(s"fact $n $ans").tell |
| 53 | + yield ans |
| 54 | + |
| 55 | + /* |
| 56 | + 9.8.3 Exercise: Hacking on Readers |
| 57 | +
|
| 58 | + The classic use of Readers is to build programs that accept a configuration as a parameter. |
| 59 | + Let's ground this with a complete example of a simple login system. |
| 60 | + Our configuration will consist of two databases: a list of valid users and a list of their passwords. |
| 61 | +
|
| 62 | + Start by creating a type alias DbReader for a Reader that consumes a Db as input. |
| 63 | +
|
| 64 | + Now create methods that generate DbReaders to look up the username for an Int user ID, |
| 65 | + and look up the password for a String username. |
| 66 | +
|
| 67 | + Finally create a checkLogin method to check the password for a given user ID. |
| 68 | + */ |
| 69 | + final case class Db( |
| 70 | + usernames: Map[Int, String], |
| 71 | + passwords: Map[String, String] |
| 72 | + ) |
| 73 | + |
| 74 | + type DbReader[A] = Reader[Db, A] |
| 75 | + |
| 76 | + def findUsername(userId: Int): DbReader[Option[String]] = |
| 77 | + Reader(_.usernames.get(userId)) |
| 78 | + |
| 79 | + def checkPassword(username: String, password: String): DbReader[Boolean] = |
| 80 | + Reader(_.passwords.get(username).contains(password)) |
| 81 | + |
| 82 | + def checkLogin(userId: Int, password: String): DbReader[Boolean] = |
| 83 | + for |
| 84 | + username <- findUsername(userId) |
| 85 | + passwordOk <- username |
| 86 | + .map(checkPassword(_, password)) |
| 87 | + .getOrElse(false.pure[DbReader]) |
| 88 | + yield passwordOk |
| 89 | + |
| 90 | + /* |
| 91 | + 9.9.3 Exercise: Post-Order Calculator |
| 92 | + Let's write an interpreter for post-order expressions. |
| 93 | + We can parse each symbol into a State instance representing |
| 94 | + a transformation on the stack and an intermediate result. |
| 95 | +
|
| 96 | + Start by writing a function evalOne that parses a single symbol into an instance of State. |
| 97 | + If the stack is in the wrong configuration, it's OK to throw an exception. |
| 98 | + */ |
| 99 | + type CalcState[A] = State[List[Int], A] |
| 100 | + |
| 101 | + def evalOne(sym: String): CalcState[Int] = |
| 102 | + sym match |
| 103 | + case "+" => operator(_ + _) |
| 104 | + case "-" => operator(_ - _) |
| 105 | + case "*" => operator(_ * _) |
| 106 | + case "/" => operator(_ / _) |
| 107 | + case num => operand(num.toInt) |
| 108 | + |
| 109 | + def operand(num: Int): CalcState[Int] = |
| 110 | + State[List[Int], Int] { stack => |
| 111 | + (num :: stack, num) |
| 112 | + } |
| 113 | + |
| 114 | + def operator(func: (Int, Int) => Int): CalcState[Int] = |
| 115 | + State[List[Int], Int]: |
| 116 | + case b :: a :: tail => |
| 117 | + val ans = func(a, b) |
| 118 | + (ans :: tail, ans) |
| 119 | + |
| 120 | + case _ => sys.error("Fail!") |
| 121 | + |
| 122 | + /* |
| 123 | + Generalise this example by writing an evalAll method that computes the result of a List[String]. |
| 124 | + Use evalOne to process each symbol, and thread the resulting State monads together using flatMap. |
| 125 | + */ |
| 126 | + def evalAll(input: List[String]): CalcState[Int] = |
| 127 | + input.foldLeft(0.pure[CalcState]) { (s, x) => |
| 128 | + // We discard the value, but must use the previous |
| 129 | + // state for the next computation. |
| 130 | + // Simply invoking evalOne will create a new state. |
| 131 | + s *> evalOne(x) |
| 132 | + } |
| 133 | + |
| 134 | + /* |
| 135 | + Complete the exercise by implementing an evalInput function that splits an input String into symbols, |
| 136 | + calls evalAll, and runs the result with an initial stack. |
| 137 | + */ |
| 138 | + def evalInput(input: String): Int = |
| 139 | + evalAll(input.split(" ").toList).runA(Nil).value |
0 commit comments