Skip to content

Commit eb1bc3e

Browse files
author
Abhijit Sarkar
committed
Complete ch09
1 parent 5b6d30d commit eb1bc3e

14 files changed

+557
-29
lines changed

.scalafmt.conf

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ project.excludePaths = [
88
"glob:**/ch04/src/**.scala",
99
"glob:**/ch06/src/Cat.scala",
1010
"glob:**/ch07/src/*.scala",
11-
"glob:**/ch08/src/*.scala"
11+
"glob:**/ch08/src/*.scala",
12+
"glob:**/ch09/src/*.scala",
13+
"glob:**/ch09/src/*.sc",
1214
]

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The older code is available in branches.
1616
6. [Using Cats](ch06)
1717
7. [Monoids and Semigroups](ch07)
1818
8. [Functors](ch08)
19+
9. [Monads](ch09)
1920

2021
## Running tests
2122
```

ch06/src/Cat.scala

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
11
package ch06
22

3-
import cats.Show
4-
import cats.instances.int.catsStdShowForInt
5-
import cats.instances.string.catsStdShowForString
6-
import cats.syntax.show.toShow
7-
import cats.Eq
8-
import cats.syntax.eq.catsSyntaxEq
3+
import cats.{Eq, Show}
4+
import cats.syntax.show.toShow // A.show if Show[A] exists
5+
import cats.syntax.eq.catsSyntaxEq // ===
96

107
final case class Cat(name: String, age: Int, color: String)
118

ch06/test/src/CatSpec.scala

+5-5
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,17 @@ import cats.syntax.eq.catsSyntaxEq
88
class CatSpec extends AnyFunSpec:
99
describe("Cat"):
1010
it("Show"):
11-
Cat("Garfield", 41, "ginger and black").show `shouldBe` "Garfield is a 41 year-old ginger and black cat."
11+
Cat("Garfield", 41, "ginger and black").show shouldBe "Garfield is a 41 year-old ginger and black cat."
1212

1313
it("Eq"):
1414
val cat1 = Cat("Garfield", 38, "orange and black")
1515
val cat2 = Cat("Heathcliff", 32, "orange and black")
1616

17-
cat1 === cat2 `shouldBe` false
18-
cat1 =!= cat2 `shouldBe` true
17+
cat1 === cat2 shouldBe false
18+
cat1 =!= cat2 shouldBe true
1919

2020
val optionCat1 = Option(cat1)
2121
val optionCat2 = Option.empty[Cat]
2222

23-
optionCat1 === optionCat2 `shouldBe` false
24-
optionCat1 =!= optionCat2 `shouldBe` true
23+
optionCat1 === optionCat2 shouldBe false
24+
optionCat1 =!= optionCat2 shouldBe true

ch07/src/Lib.scala

+2-3
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ package ch07
44
We can use Semigroups and Monoids by importing two things: the type classes themselves,
55
and the semigroup syntax to give us the |+| operator.
66
*/
7-
import cats.{Monoid as CatsMonoid}
8-
import cats.syntax.semigroup.catsSyntaxSemigroup
7+
import cats.syntax.semigroup.catsSyntaxSemigroup // A |+| A if Semigroup[A] exists
98

109
object Lib:
1110

@@ -19,7 +18,7 @@ object Lib:
1918
People now want to add List[Option[Int]]. Change add so this is possible. The SuperAdder code base
2019
is of the highest quality, so make sure there is no code duplication!
2120
*/
22-
def add[A: CatsMonoid as m](items: List[A]): A =
21+
def add[A: cats.Monoid as m](items: List[A]): A =
2322
items.foldLeft(m.empty)(_ |+| _)
2423

2524
// import cats.instances.int.catsKernelStdGroupForInt

ch07/test/src/LibSpec.scala

+4-4
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,16 @@ class LibSpec extends AnyFunSpec:
77
describe("Monoid"):
88
it("should add integers"):
99
val ints = List(1, 2, 3)
10-
add(ints) `shouldBe` 6
10+
add(ints) shouldBe 6
1111

1212
it("should add strings"):
1313
val strings = List("Hi ", "there")
14-
add(strings) `shouldBe` "Hi there"
14+
add(strings) shouldBe "Hi there"
1515

1616
it("should add sets"):
1717
val sets = List(Set("A", "B"), Set("B", "C"))
18-
add(sets) `shouldBe` Set("A", "B", "C")
18+
add(sets) shouldBe Set("A", "B", "C")
1919

2020
it("should add options"):
2121
val opts = List(Option(22), Option(20))
22-
add(opts) `shouldBe` Option(42)
22+
add(opts) shouldBe Option(42)

ch08/src/Tree.scala

+7-7
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@ package ch08
22

33
import cats.Functor
44

5-
sealed trait Tree[+A]
6-
7-
final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]
8-
9-
final case class Leaf[A](value: A) extends Tree[A]
5+
enum Tree[A]:
6+
case Leaf(value: A)
7+
case Branch(left: Tree[A], right: Tree[A])
108

119
object Tree:
10+
import Tree.*
11+
1212
/* 8.5.4 Exercise: Branching out with Functors
1313
Write a Functor for the following binary tree data type.
1414
Verify that the code works as expected on instances of Branch and Leaf.
@@ -19,8 +19,8 @@ object Tree:
1919
case Branch(l, r) => Branch(map(l)(f), map(r)(f))
2020
case Leaf(value) => Leaf(f(value))
2121

22-
// The compiler can find a Functor instance for Tree but not for Branch or Leaf (Functor is invariant in F).
23-
// Let's add some smart constructors to compensate.
22+
// The compiler can find a Functor instance for Tree but not for Branch or Leaf
23+
// (Functor is invariant in F). Let's add some smart constructors to compensate.
2424
def branch[A](left: Tree[A], right: Tree[A]): Tree[A] =
2525
Branch(left, right)
2626

ch08/test/src/TreeSpec.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
package ch08
22
import org.scalatest.funspec.AnyFunSpec
33
import org.scalatest.matchers.should.Matchers.shouldBe
4-
import cats.syntax.functor.toFunctorOps
4+
import cats.syntax.functor.toFunctorOps // map
55

66
class TreeSpec extends AnyFunSpec:
77
describe("Tree Functor"):
88
it("should map on leaf"):
99
val actual = Tree.leaf(100).map(_ * 2)
10-
actual `shouldBe` Leaf(200)
10+
actual shouldBe Tree.leaf(200)
1111

1212
it("should map on branch"):
1313
val actual = Tree.branch(Tree.leaf(10), Tree.leaf(20)).map(_ * 2)
14-
actual `shouldBe` Branch(Leaf(20), Leaf(40))
14+
actual shouldBe Tree.branch(Tree.leaf(20), Tree.leaf(40))

ch09/src/Lib.scala

+139
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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

ch09/src/Monad.scala

+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package ch09
2+
3+
trait Monad[F[_]]:
4+
def pure[A](a: A): F[A]
5+
6+
def flatMap[A, B](value: F[A])(f: A => F[B]): F[B]
7+
8+
/*
9+
9.1.5 Exercise: Getting Func-y
10+
Every monad is also a functor. We can define map in the same way
11+
for every monad using the existing methods, flatMap and pure.
12+
Try defining map yourself now.
13+
*/
14+
def map[A, B](value: F[A])(f: A => B): F[B] =
15+
flatMap(value)(a => pure(f(a)))
16+
17+
object Monad:
18+
type Id[A] = A
19+
20+
/*
21+
9.3.1 Exercise: Monadic Secret Identities
22+
Implement pure, map, and flatMap for Id!
23+
*/
24+
given idMonad: Monad[Id]:
25+
def pure[A](a: A): Id[A] = a
26+
27+
def flatMap[A, B](value: Id[A])(f: A => Id[B]): Id[B] =
28+
f(value)

ch09/src/Tree.scala

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package ch09
2+
3+
/*
4+
9.10.1 Exercise: Branching out Further with Monads
5+
6+
Let's write a Monad for the Tree data type given below.
7+
8+
Verify that the code works on instances of Branch and Leaf,
9+
and that the Monad provides Functor-like behaviour for free.
10+
11+
Also verify that having a Monad in scope allows us to use for comprehensions,
12+
despite the fact that we haven’t directly implemented flatMap or map on Tree.
13+
14+
Don't feel you have to make tailRecM tail-recursive. Doing so is quite difficult.
15+
*/
16+
enum Tree[A]:
17+
case Leaf(value: A)
18+
case Branch(left: Tree[A], right: Tree[A])
19+
20+
object Tree:
21+
import Tree.*
22+
23+
given cats.Monad[Tree]:
24+
override def pure[A](x: A): Tree[A] =
25+
Leaf(x)
26+
27+
override def flatMap[A, B](t: Tree[A])(f: A => Tree[B]): Tree[B] =
28+
t match
29+
case Leaf(x) => f(x)
30+
case Branch(l, r) => Branch(flatMap(l)(f), flatMap(r)(f))
31+
32+
// Not stack-safe!
33+
override def tailRecM[A, B](a: A)(f: A => Tree[Either[A, B]]): Tree[B] =
34+
flatMap(f(a)):
35+
case Left(value) => tailRecM(value)(f)
36+
case Right(value) => Leaf(value)
37+
38+
// Smart constructors to help the compiler.
39+
def branch[A](left: Tree[A], right: Tree[A]): Tree[A] =
40+
Branch(left, right)
41+
42+
def leaf[A](value: A): Tree[A] =
43+
Leaf(value)

0 commit comments

Comments
 (0)