Skip to content

Commit

Permalink
Complete ch09
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Dec 24, 2024
1 parent 5b6d30d commit eb1bc3e
Show file tree
Hide file tree
Showing 14 changed files with 557 additions and 29 deletions.
4 changes: 3 additions & 1 deletion .scalafmt.conf
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ project.excludePaths = [
"glob:**/ch04/src/**.scala",
"glob:**/ch06/src/Cat.scala",
"glob:**/ch07/src/*.scala",
"glob:**/ch08/src/*.scala"
"glob:**/ch08/src/*.scala",
"glob:**/ch09/src/*.scala",
"glob:**/ch09/src/*.sc",
]
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The older code is available in branches.
6. [Using Cats](ch06)
7. [Monoids and Semigroups](ch07)
8. [Functors](ch08)
9. [Monads](ch09)

## Running tests
```
Expand Down
9 changes: 3 additions & 6 deletions ch06/src/Cat.scala
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package ch06

import cats.Show
import cats.instances.int.catsStdShowForInt
import cats.instances.string.catsStdShowForString
import cats.syntax.show.toShow
import cats.Eq
import cats.syntax.eq.catsSyntaxEq
import cats.{Eq, Show}
import cats.syntax.show.toShow // A.show if Show[A] exists
import cats.syntax.eq.catsSyntaxEq // ===

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

Expand Down
10 changes: 5 additions & 5 deletions ch06/test/src/CatSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@ import cats.syntax.eq.catsSyntaxEq
class CatSpec extends AnyFunSpec:
describe("Cat"):
it("Show"):
Cat("Garfield", 41, "ginger and black").show `shouldBe` "Garfield is a 41 year-old ginger and black cat."
Cat("Garfield", 41, "ginger and black").show shouldBe "Garfield is a 41 year-old ginger and black cat."

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

cat1 === cat2 `shouldBe` false
cat1 =!= cat2 `shouldBe` true
cat1 === cat2 shouldBe false
cat1 =!= cat2 shouldBe true

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

optionCat1 === optionCat2 `shouldBe` false
optionCat1 =!= optionCat2 `shouldBe` true
optionCat1 === optionCat2 shouldBe false
optionCat1 =!= optionCat2 shouldBe true
5 changes: 2 additions & 3 deletions ch07/src/Lib.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ package ch07
We can use Semigroups and Monoids by importing two things: the type classes themselves,
and the semigroup syntax to give us the |+| operator.
*/
import cats.{Monoid as CatsMonoid}
import cats.syntax.semigroup.catsSyntaxSemigroup
import cats.syntax.semigroup.catsSyntaxSemigroup // A |+| A if Semigroup[A] exists

object Lib:

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

// import cats.instances.int.catsKernelStdGroupForInt
Expand Down
8 changes: 4 additions & 4 deletions ch07/test/src/LibSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ class LibSpec extends AnyFunSpec:
describe("Monoid"):
it("should add integers"):
val ints = List(1, 2, 3)
add(ints) `shouldBe` 6
add(ints) shouldBe 6

it("should add strings"):
val strings = List("Hi ", "there")
add(strings) `shouldBe` "Hi there"
add(strings) shouldBe "Hi there"

it("should add sets"):
val sets = List(Set("A", "B"), Set("B", "C"))
add(sets) `shouldBe` Set("A", "B", "C")
add(sets) shouldBe Set("A", "B", "C")

it("should add options"):
val opts = List(Option(22), Option(20))
add(opts) `shouldBe` Option(42)
add(opts) shouldBe Option(42)
14 changes: 7 additions & 7 deletions ch08/src/Tree.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ package ch08

import cats.Functor

sealed trait Tree[+A]

final case class Branch[A](left: Tree[A], right: Tree[A]) extends Tree[A]

final case class Leaf[A](value: A) extends Tree[A]
enum Tree[A]:
case Leaf(value: A)
case Branch(left: Tree[A], right: Tree[A])

object Tree:
import Tree.*

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

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

Expand Down
6 changes: 3 additions & 3 deletions ch08/test/src/TreeSpec.scala
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
package ch08
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
import cats.syntax.functor.toFunctorOps
import cats.syntax.functor.toFunctorOps // map

class TreeSpec extends AnyFunSpec:
describe("Tree Functor"):
it("should map on leaf"):
val actual = Tree.leaf(100).map(_ * 2)
actual `shouldBe` Leaf(200)
actual shouldBe Tree.leaf(200)

it("should map on branch"):
val actual = Tree.branch(Tree.leaf(10), Tree.leaf(20)).map(_ * 2)
actual `shouldBe` Branch(Leaf(20), Leaf(40))
actual shouldBe Tree.branch(Tree.leaf(20), Tree.leaf(40))
139 changes: 139 additions & 0 deletions ch09/src/Lib.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package ch09

import cats.{Eval, MonadError}
import cats.data.{Reader, Writer, State}
import cats.syntax.applicative.catsSyntaxApplicativeId // pure
import cats.syntax.writer.catsSyntaxWriterId // tell
import cats.syntax.apply.catsSyntaxApplyOps // *>

object Lib:
/*
9.5.4 Exercise: Abstracting
Implement a method validateAdult with the following signature
*/
// type MonadThrow[F[_]] = MonadError[F, Throwable]
// def validateAdult[F[_] : MonadThrow as me](age: Int): F[Int]

// def validateAdult[F[_] : ([G[_]] =>> MonadError[G, Throwable]) as me](age: Int): F[Int] =
def validateAdult[F[_]](age: Int)(using me: MonadError[F, Throwable]): F[Int] =
me.ensure(me.pure(age))(IllegalArgumentException("Age must be greater than or equal to 18"))(_ >= 18)

/*
9.6.5 Exercise: Safer Folding using Eval
The naive implementation of foldRight below is not stack safe. Make it so using Eval.
*/
def foldRight[A, B](as: List[A], acc: B)(fn: (A, B) => B): B =
def foldR(xs: List[A]): Eval[B] =
xs match
case head :: tail => Eval.defer(foldR(tail).map(fn(head, _)))
case Nil => Eval.now(acc)

foldR(as).value

/*
9.7.3 Exercise: Show Your Working
Rewrite factorial so it captures the log messages in a Writer.
Demonstrate that this allows us to reliably separate the logs for concurrent computations.
*/
def slowly[A](body: => A): A =
try body
finally Thread.sleep(100)

type Logged[A] = Writer[Vector[String], A]

def factorial(n: Int): Logged[Int] =
for
ans <-
if (n == 0)
then 1.pure[Logged]
else slowly(factorial(n - 1).map(_ * n))
// The log in a Writer is preserved when we map or flatMap over it.
_ <- Vector(s"fact $n $ans").tell
yield ans

/*
9.8.3 Exercise: Hacking on Readers
The classic use of Readers is to build programs that accept a configuration as a parameter.
Let's ground this with a complete example of a simple login system.
Our configuration will consist of two databases: a list of valid users and a list of their passwords.
Start by creating a type alias DbReader for a Reader that consumes a Db as input.
Now create methods that generate DbReaders to look up the username for an Int user ID,
and look up the password for a String username.
Finally create a checkLogin method to check the password for a given user ID.
*/
final case class Db(
usernames: Map[Int, String],
passwords: Map[String, String]
)

type DbReader[A] = Reader[Db, A]

def findUsername(userId: Int): DbReader[Option[String]] =
Reader(_.usernames.get(userId))

def checkPassword(username: String, password: String): DbReader[Boolean] =
Reader(_.passwords.get(username).contains(password))

def checkLogin(userId: Int, password: String): DbReader[Boolean] =
for
username <- findUsername(userId)
passwordOk <- username
.map(checkPassword(_, password))
.getOrElse(false.pure[DbReader])
yield passwordOk

/*
9.9.3 Exercise: Post-Order Calculator
Let's write an interpreter for post-order expressions.
We can parse each symbol into a State instance representing
a transformation on the stack and an intermediate result.
Start by writing a function evalOne that parses a single symbol into an instance of State.
If the stack is in the wrong configuration, it's OK to throw an exception.
*/
type CalcState[A] = State[List[Int], A]

def evalOne(sym: String): CalcState[Int] =
sym match
case "+" => operator(_ + _)
case "-" => operator(_ - _)
case "*" => operator(_ * _)
case "/" => operator(_ / _)
case num => operand(num.toInt)

def operand(num: Int): CalcState[Int] =
State[List[Int], Int] { stack =>
(num :: stack, num)
}

def operator(func: (Int, Int) => Int): CalcState[Int] =
State[List[Int], Int]:
case b :: a :: tail =>
val ans = func(a, b)
(ans :: tail, ans)

case _ => sys.error("Fail!")

/*
Generalise this example by writing an evalAll method that computes the result of a List[String].
Use evalOne to process each symbol, and thread the resulting State monads together using flatMap.
*/
def evalAll(input: List[String]): CalcState[Int] =
input.foldLeft(0.pure[CalcState]) { (s, x) =>
// We discard the value, but must use the previous
// state for the next computation.
// Simply invoking evalOne will create a new state.
s *> evalOne(x)
}

/*
Complete the exercise by implementing an evalInput function that splits an input String into symbols,
calls evalAll, and runs the result with an initial stack.
*/
def evalInput(input: String): Int =
evalAll(input.split(" ").toList).runA(Nil).value
28 changes: 28 additions & 0 deletions ch09/src/Monad.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package ch09

trait Monad[F[_]]:
def pure[A](a: A): F[A]

def flatMap[A, B](value: F[A])(f: A => F[B]): F[B]

/*
9.1.5 Exercise: Getting Func-y
Every monad is also a functor. We can define map in the same way
for every monad using the existing methods, flatMap and pure.
Try defining map yourself now.
*/
def map[A, B](value: F[A])(f: A => B): F[B] =
flatMap(value)(a => pure(f(a)))

object Monad:
type Id[A] = A

/*
9.3.1 Exercise: Monadic Secret Identities
Implement pure, map, and flatMap for Id!
*/
given idMonad: Monad[Id]:
def pure[A](a: A): Id[A] = a

def flatMap[A, B](value: Id[A])(f: A => Id[B]): Id[B] =
f(value)
43 changes: 43 additions & 0 deletions ch09/src/Tree.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package ch09

/*
9.10.1 Exercise: Branching out Further with Monads
Let's write a Monad for the Tree data type given below.
Verify that the code works on instances of Branch and Leaf,
and that the Monad provides Functor-like behaviour for free.
Also verify that having a Monad in scope allows us to use for comprehensions,
despite the fact that we haven’t directly implemented flatMap or map on Tree.
Don't feel you have to make tailRecM tail-recursive. Doing so is quite difficult.
*/
enum Tree[A]:
case Leaf(value: A)
case Branch(left: Tree[A], right: Tree[A])

object Tree:
import Tree.*

given cats.Monad[Tree]:
override def pure[A](x: A): Tree[A] =
Leaf(x)

override def flatMap[A, B](t: Tree[A])(f: A => Tree[B]): Tree[B] =
t match
case Leaf(x) => f(x)
case Branch(l, r) => Branch(flatMap(l)(f), flatMap(r)(f))

// Not stack-safe!
override def tailRecM[A, B](a: A)(f: A => Tree[Either[A, B]]): Tree[B] =
flatMap(f(a)):
case Left(value) => tailRecM(value)(f)
case Right(value) => Leaf(value)

// Smart constructors to help the compiler.
def branch[A](left: Tree[A], right: Tree[A]): Tree[A] =
Branch(left, right)

def leaf[A](value: A): Tree[A] =
Leaf(value)
Loading

0 comments on commit eb1bc3e

Please sign in to comment.