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

Lines changed: 5 additions & 1 deletion
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

Lines changed: 2 additions & 2 deletions
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]
Lines changed: 16 additions & 0 deletions
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)
Lines changed: 14 additions & 0 deletions
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)
Lines changed: 35 additions & 0 deletions
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

Lines changed: 84 additions & 0 deletions
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
Lines changed: 58 additions & 0 deletions
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)
Lines changed: 21 additions & 0 deletions
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

Lines changed: 27 additions & 0 deletions
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
Lines changed: 39 additions & 0 deletions
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)

0 commit comments

Comments
 (0)