Skip to content

Commit c5ce581

Browse files
author
Abhijit Sarkar
committed
Complete book
1 parent 4a4c96f commit c5ce581

20 files changed

+416
-45
lines changed

README.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@ Official GitHub repo: https://github.com/scalawithcats/scala-with-cats
1212
4. [Monads](src/main/scala/ch04)
1313
5. [Monad Transformers](src/main/scala/ch05)
1414
6. [Semigroupal and Applicative](src/main/scala/ch06)
15-
7. [Foldable and Traverse]()(src/main/scala/ch07)
15+
7. [Foldable and Traverse](src/main/scala/ch07)
16+
8. [Case Study: Testing Asynchronous Code](src/main/scala/ch08)
17+
9. [Case Study: Map-Reduce](src/main/scala/ch09)
18+
10. [Case Study: Data Validation](src/main/scala/ch10)
19+
11. [Case Study: CRDTs](src/main/scala/ch11)
1620

1721
## Running tests
1822

src/main/scala/ch04/MonadError.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ import cats.syntax.applicativeError.catsSyntaxApplicativeErrorId
77
4.5.4 Exercise: Abstracting
88
Implement a method validateAdult with the following signature
99
10-
def validateAdult[F[_]](age: Int)(implicit me: MonadError[F, Throwable]): F[Int]
10+
def validateAdult[F[_]](age: Int)(using me: MonadError[F, Throwable]): F[Int]
1111
1212
When passed an age greater than or equal to 18 it should return that value as a success.
1313
Otherwise it should return a error represented as an IllegalArgumentException.
1414
*/
1515
object MonadError:
16-
def validateAdult[F[_]](age: Int)(implicit me: CatsMonadError[F, Throwable]): F[Int] =
16+
def validateAdult[F[_]](age: Int)(using me: CatsMonadError[F, Throwable]): F[Int] =
1717
if age >= 18
1818
then Monad[F].pure(age)
1919
else new IllegalArgumentException("Age must be greater than or equal to 18").raiseError[F, Int]
+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package ch08
2+
3+
import cats.Monad
4+
import scala.concurrent.Future
5+
import cats.Id
6+
7+
// 8 Case Study: Testing Asynchronous Code
8+
trait UptimeClient[F[_]: Monad]:
9+
def getUptime(hostname: String): F[Int]
10+
11+
trait RealUptimeClient extends UptimeClient[Future]:
12+
def getUptime(hostname: String): Future[Int]
13+
14+
class TestUptimeClient(hosts: Map[String, Int]) extends UptimeClient[Id]:
15+
def getUptime(hostname: String): Int =
16+
hosts.getOrElse(hostname, 0)
+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package ch08
2+
3+
import cats.syntax.traverse.toTraverseOps
4+
import cats.syntax.functor.toFunctorOps
5+
import cats.Applicative
6+
7+
// traverse only works on sequences of values that have an Applicative.
8+
// In our original code we were traversing a List[Future[Int]].
9+
// There is an applicative for Future so that was fine.
10+
// In this version we are traversing a List[F[Int]].
11+
// We need to prove to the compiler that F has an Applicative.
12+
class UptimeService[F[_]: Applicative](client: UptimeClient[F]):
13+
def getTotalUptime(hostnames: List[String]): F[Int] =
14+
hostnames.traverse(client.getUptime).map(_.sum)

src/main/scala/ch09/MapReduce.scala

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package ch09
2+
3+
import cats.Monoid
4+
import scala.concurrent.Future
5+
import cats.syntax.traverse.toTraverseOps
6+
import cats.syntax.foldable.toFoldableOps
7+
import scala.concurrent.ExecutionContext
8+
9+
/*
10+
9 Case Study: Map-Reduce
11+
12+
1. Start with an initial list of all the data we need to process
13+
2. Divide the data into batches, sending one batch to each CPU
14+
3. The CPUs run a batch-level map phase in parallel
15+
4. The CPUs run a batch-level reduce phase in parallel,
16+
producing a local result for each batch
17+
5. Reduce the results for each batch to a single final result
18+
*/
19+
object MapReduce:
20+
def parallelFoldMap[A, B: Monoid](values: Vector[A])(func: A => B)(using ExecutionContext): Future[B] =
21+
val numCores = Runtime.getRuntime.availableProcessors
22+
val groupSize = (1.0 * values.size / numCores).ceil.toInt
23+
24+
values
25+
.grouped(groupSize)
26+
// grouped returns an Iterator but cats
27+
// doesn't have a Traverse instance for Iterator,
28+
// so, convert to Vector.
29+
.toVector
30+
// ExecutionContext.Implicits.global. This default context allocates
31+
// a thread pool with one thread per CPU in our machine.
32+
// When we create a Future the ExecutionContext schedules it for execution.
33+
// If there is a free thread in the pool, the Future starts executing immediately.
34+
.traverse(group => Future(group.foldMap(func)))
35+
.map(_.combineAll)

src/main/scala/ch10/Check.scala

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package ch10
2+
3+
import cats.data.NonEmptyList
4+
import cats.data.Kleisli
5+
import cats.syntax.apply.catsSyntaxTuple2Semigroupal
6+
import Predicate.*
7+
8+
type Errors = NonEmptyList[String]
9+
10+
def error(s: String): NonEmptyList[String] =
11+
NonEmptyList(s, Nil)
12+
13+
type Result[A] = Either[Errors, A]
14+
15+
// Kleisli lets us sequence monadic transforms,
16+
// A => F[B] `flatMap` B => F[C]
17+
type Check[A, B] = Kleisli[Result, A, B]
18+
19+
def check[A, B](func: A => Result[B]): Check[A, B] =
20+
Kleisli(func)
21+
22+
def checkPred[A](pred: Predicate[Errors, A]): Check[A, A] =
23+
// Running the predicate produces a func of type:
24+
// A => Either[NonEmptyList[String], A]
25+
// = A => Result[A]
26+
//
27+
// We must be able to convert a Predicate to a function,
28+
// as Kleisli only works with functions.
29+
// When we convert a Predicate to a function,
30+
// it should have type A => Either[E, A] rather than
31+
// A => Validated[E, A] because Kleisli relies on the
32+
// wrapped function returning a monad.
33+
Kleisli[Result, A, A](pred.run)
34+
35+
def longerThan(n: Int): Predicate[Errors, String] =
36+
Predicate.lift(
37+
error(s"Must be longer than $n characters"),
38+
str => str.size > n
39+
)
40+
41+
val alphanumeric: Predicate[Errors, String] =
42+
Predicate.lift(
43+
error(s"Must be all alphanumeric characters"),
44+
str => str.forall(_.isLetterOrDigit)
45+
)
46+
47+
def contains(char: Char): Predicate[Errors, String] =
48+
Predicate.lift(
49+
error(s"Must contain the character $char"),
50+
str => str.contains(char)
51+
)
52+
53+
def containsOnce(char: Char): Predicate[Errors, String] =
54+
Predicate.lift(
55+
error(s"Must contain the character $char only once"),
56+
str => str.filter(_ == char).size == 1
57+
)
58+
59+
// Kleisli[[A] =>> Either[Errors, A], String, String]
60+
val checkUsername: Check[String, String] =
61+
checkPred(longerThan(3) `and` alphanumeric)
62+
63+
val splitEmail: Check[String, (String, String)] =
64+
check(_.split('@') match {
65+
case Array(name, domain) =>
66+
Right((name, domain))
67+
68+
case _ =>
69+
Left(error("Must contain a single @ character"))
70+
})
71+
72+
val checkLeft: Check[String, String] =
73+
checkPred(longerThan(0))
74+
75+
val checkRight: Check[String, String] =
76+
checkPred(longerThan(3) `and` contains('.'))
77+
78+
val joinEmail: Check[(String, String), String] =
79+
check:
80+
case (l, r) =>
81+
(checkLeft(l), checkRight(r)).mapN(_ + "@" + _)
82+
83+
val checkEmail: Check[String, String] =
84+
splitEmail `andThen` joinEmail

src/main/scala/ch10/Predicate.scala

+58
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package ch10
2+
3+
import cats.kernel.Semigroup
4+
import cats.data.Validated
5+
import cats.syntax.apply.catsSyntaxTuple2Semigroupal
6+
import cats.syntax.validated.catsSyntaxValidatedId
7+
import cats.syntax.semigroup.catsSyntaxSemigroup
8+
import cats.data.Validated.{Invalid, Valid}
9+
10+
// 10 Case Study: Data Validation
11+
12+
// Preciate is basically a wrapper around a function:
13+
// A => Validated[E, A]
14+
sealed trait Predicate[E, A]:
15+
import Predicate.*
16+
17+
def and(that: Predicate[E, A]): Predicate[E, A] =
18+
And(this, that)
19+
20+
def or(that: Predicate[E, A]): Predicate[E, A] =
21+
Or(this, that)
22+
23+
def run(using s: Semigroup[E]): A => Either[E, A] =
24+
(a: A) => this(a).toEither
25+
26+
private def apply(a: A)(using s: Semigroup[E]): Validated[E, A] =
27+
this match
28+
case Pure(func) =>
29+
func(a)
30+
31+
case And(left, right) =>
32+
(left(a), right(a)).mapN((_, _) => a)
33+
34+
case Or(left, right) =>
35+
left(a) match
36+
case Valid(_) => Valid(a)
37+
case Invalid(e1) =>
38+
right(a) match
39+
case Valid(_) => Valid(a)
40+
case Invalid(e2) => Invalid(e1 |+| e2)
41+
42+
object Predicate:
43+
private final case class And[E, A](
44+
left: Predicate[E, A],
45+
right: Predicate[E, A]
46+
) extends Predicate[E, A]
47+
48+
private final case class Or[E, A](
49+
left: Predicate[E, A],
50+
right: Predicate[E, A]
51+
) extends Predicate[E, A]
52+
53+
private final case class Pure[E, A](
54+
func: A => Validated[E, A]
55+
) extends Predicate[E, A]
56+
57+
def lift[E, A](err: E, fn: A => Boolean): Predicate[E, A] =
58+
Pure(a => if (fn(a)) a.valid else err.invalid)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package ch11
2+
3+
import cats.kernel.CommutativeMonoid
4+
5+
trait BoundedSemiLattice[A] extends CommutativeMonoid[A]:
6+
def combine(a1: A, a2: A): A
7+
def empty: A
8+
9+
object BoundedSemiLattice:
10+
given intInstance: BoundedSemiLattice[Int] with
11+
def combine(a1: Int, a2: Int): Int =
12+
a1 max a2
13+
14+
val empty: Int = 0
15+
16+
// given [A]: BoundedSemiLattice[Set[A]] with
17+
// def combine(a1: Set[A], a2: Set[A]): Set[A] =
18+
// a1 union a2
19+
20+
// val empty: Set[A] =
21+
// Set.empty[A]

src/main/scala/ch11/GCounter.scala

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package ch11
2+
3+
import cats.kernel.CommutativeMonoid
4+
import cats.syntax.semigroup.catsSyntaxSemigroup
5+
import cats.syntax.foldable.toFoldableOps
6+
7+
// 11 Case Study: CRDTs
8+
trait GCounter[F[_, _], K, V]:
9+
def increment(f: F[K, V])(k: K, v: V)(using CommutativeMonoid[V]): F[K, V]
10+
11+
def merge(f1: F[K, V], f2: F[K, V])(using BoundedSemiLattice[V]): F[K, V]
12+
13+
def total(f: F[K, V])(using CommutativeMonoid[V]): V
14+
15+
object GCounter:
16+
import KeyValueStoreSyntax.*
17+
18+
given [F[_, _], K, V](using KeyValueStore[F], CommutativeMonoid[F[K, V]]): GCounter[F, K, V] with
19+
def increment(f: F[K, V])(key: K, value: V)(using m: CommutativeMonoid[V]): F[K, V] =
20+
val total = f.getOrElse(key, m.empty) |+| value
21+
f.put(key, total)
22+
23+
def merge(f1: F[K, V], f2: F[K, V])(using BoundedSemiLattice[V]): F[K, V] =
24+
f1 |+| f2
25+
26+
def total(f: F[K, V])(using CommutativeMonoid[V]): V =
27+
f.values.combineAll
+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package ch11
2+
3+
trait KeyValueStore[F[_, _]]:
4+
def put[K, V](f: F[K, V])(k: K, v: V): F[K, V]
5+
6+
def get[K, V](f: F[K, V])(k: K): Option[V]
7+
8+
def getOrElse[K, V](f: F[K, V])(k: K, default: V): V =
9+
get(f)(k).getOrElse(default)
10+
11+
def values[K, V](f: F[K, V]): List[V]
12+
13+
object KeyValueStore:
14+
given KeyValueStore[Map] with
15+
def put[K, V](f: Map[K, V])(k: K, v: V): Map[K, V] =
16+
f + (k -> v)
17+
18+
def get[K, V](f: Map[K, V])(k: K): Option[V] =
19+
f.get(k)
20+
21+
override def getOrElse[K, V](f: Map[K, V])(k: K, default: V): V =
22+
f.getOrElse(k, default)
23+
24+
def values[K, V](f: Map[K, V]): List[V] =
25+
f.values.toList
26+
27+
object KeyValueStoreSyntax:
28+
extension [F[_, _], K, V](f: F[K, V])(using kvs: KeyValueStore[F])
29+
def put(key: K, value: V) =
30+
kvs.put(f)(key, value)
31+
32+
def get(key: K): Option[V] =
33+
kvs.get(f)(key)
34+
35+
def getOrElse(key: K, default: V): V =
36+
kvs.getOrElse(f)(key, default)
37+
38+
def values: List[V] =
39+
kvs.values(f)

src/test/scala/ch02/MonoidSpec.scala

+12-13
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,18 @@ import org.scalatest.matchers.should.Matchers.shouldBe
44
import ch02.Lib.add
55

66
class MonoidSpec extends AnyFunSpec:
7-
describe("Monoid"):
8-
it("should be able to add integers"):
9-
val ints = List(1, 2, 3)
10-
add(ints) `shouldBe` 6
7+
it("should add integers"):
8+
val ints = List(1, 2, 3)
9+
add(ints) `shouldBe` 6
1110

12-
it("should be able to add strings"):
13-
val strings = List("Hi ", "there")
14-
add(strings) `shouldBe` "Hi there"
11+
it("should add strings"):
12+
val strings = List("Hi ", "there")
13+
add(strings) `shouldBe` "Hi there"
1514

16-
it("should be able to add sets"):
17-
val sets = List(Set("A", "B"), Set("B", "C"))
18-
add(sets) `shouldBe` Set("A", "B", "C")
15+
it("should add sets"):
16+
val sets = List(Set("A", "B"), Set("B", "C"))
17+
add(sets) `shouldBe` Set("A", "B", "C")
1918

20-
it("should be able to add options"):
21-
val opts = List(Option(22), Option(20))
22-
add(opts) `shouldBe` Option(42)
19+
it("should add options"):
20+
val opts = List(Option(22), Option(20))
21+
add(opts) `shouldBe` Option(42)

src/test/scala/ch03/TreeSpec.scala

+3-3
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import org.scalatest.matchers.should.Matchers.shouldBe
44
import cats.syntax.functor.toFunctorOps
55

66
class TreeSpec extends AnyFunSpec:
7-
describe("Tree"):
8-
it("should be able to map on leaf"):
7+
describe("Tree functor"):
8+
it("should map on leaf"):
99
val actual = Tree.leaf(100).map(_ * 2)
1010
actual `shouldBe` Leaf(200)
1111

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

src/test/scala/ch04/ReaderSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import org.scalatest.funspec.AnyFunSpec
33
import org.scalatest.matchers.should.Matchers.shouldBe
44

55
class ReaderSpec extends AnyFunSpec:
6-
it("DbReader should be able to check password"):
6+
it("DbReader should check password"):
77
val users = Map(
88
1 -> "dade",
99
2 -> "kate",

src/test/scala/ch04/StateSpec.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,5 @@ import org.scalatest.funspec.AnyFunSpec
44
import org.scalatest.matchers.should.Matchers.shouldBe
55

66
class StateSpec extends AnyFunSpec:
7-
it("evalInput should be able to evaluate a post-order expression"):
7+
it("evalInput should evaluate a post-order expression"):
88
State.evalInput("1 2 + 3 4 + *") `shouldBe` 21

0 commit comments

Comments
 (0)