Skip to content

Commit

Permalink
Complete till chapter 3: Functors
Browse files Browse the repository at this point in the history
  • Loading branch information
Abhijit Sarkar committed Jan 12, 2024
1 parent df79291 commit 5e3a9b8
Show file tree
Hide file tree
Showing 17 changed files with 351 additions and 8 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ Official GitHub repo: https://github.com/scalawithcats/scala-with-cats

## Contents

1. [Introduction](src/main/scala/intro/)
1. [Introduction](src/main/scala/ch01)
2. [Monoids and Semigroups](src/main/scala/ch02)
3. [Functors](src/main/scala/ch03)

## Running tests

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package intro.eq
package ch01.eq

import cats.Eq
import cats.syntax.eq.catsSyntaxEq
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package intro.printable
package ch01.printable

// This is a standard use of the type class pattern.
// First we define a set of custom data types for our application.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package intro.printable
package ch01.printable

/*
1.3 Exercise: Printable Library.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package intro.show
package ch01.show

import cats.Show
import cats.instances.int.catsStdShowForInt
Expand Down
46 changes: 46 additions & 0 deletions src/main/scala/ch02/Lib.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package ch02
import cats.{Monoid as CatsMonoid}
import cats.syntax.semigroup.catsSyntaxSemigroup

object Lib:

/*
2.5.4 Exercise: Adding All The Things
The cutting edge SuperAdder v3.5a-32 is the world's first choice for adding together numbers.
The main function in the program has signature def add(items: List[Int]): Int.
In a tragic accident this code is deleted! Rewrite the method and save the day!
SuperAdder's market share continues to grow, and now there is demand for additional functionality.
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](items: List[A]): A =
items.foldLeft(CatsMonoid[A].empty)(_ |+| _)

// import cats.instances.int.catsKernelStdGroupForInt
// add(List(1, 2, 3))

// add(List(Some(1), None, Some(2), None, Some(3)))

// Doesn't compile: No given instance of type cats.kernel.Monoid[Some[Int]] was found.
// The inferred type of the list is List[Some[Int]], Cats Monoid is invariant, so,
// Monoid[Option[A]] is not applicable.
// add(List(Some(1), Some(2), Some(3)))

/*
SuperAdder is entering the POS (point-of-sale, not the other POS) market.
Now we want to add up Orders.
We need to release this code really soon so we can’t make any modifications to add.
Make it so!
*/
case class Order(totalCost: Double, quantity: Double)

given Monoid[Order] with
def combine(o1: Order, o2: Order) =
Order(
o1.totalCost + o2.totalCost,
o1.quantity + o2.quantity
)

def empty = Order(0, 0)
50 changes: 50 additions & 0 deletions src/main/scala/ch02/Monoid.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ch02

trait Monoid[A] extends Semigroup[A]:
def empty: A

object Monoid:
def apply[A](using monoid: Monoid[A]): Monoid[A] =
monoid

object MonoidInstances:
/*
2.3 Exercise: The Truth About Monoids
Consider Boolean. How many monoids can you define for this type?
*/
given booleanAndMonoid: Monoid[Boolean] with
def combine(a: Boolean, b: Boolean) = a && b
def empty = true

given booleanOrMonoid: Monoid[Boolean] with
def combine(a: Boolean, b: Boolean) = a || b
def empty = false

given booleanXorMonoid: Monoid[Boolean] with
def combine(a: Boolean, b: Boolean) =
a != b
def empty = false

// The negation of XOR
given booleanXnorMonoid: Monoid[Boolean] with
def combine(a: Boolean, b: Boolean) =
(!a || b) && (a || !b)

def empty = true

/*
2.4 Exercise: All Set for Monoids
What monoids and semigroups are there for sets?
*/
given setUnionMonoid[A]: Monoid[Set[A]] with
def combine(a: Set[A], b: Set[A]) = a union b
def empty = Set.empty[A]

given symDiffMonoid[A]: Monoid[Set[A]] with
def combine(a: Set[A], b: Set[A]): Set[A] =
(a diff b) union (b diff a)
def empty: Set[A] = Set.empty

// given intAdditionMonoid: Monoid[Int] with
// def combine(x: Int, y: Int): Int = x + y
// def empty: Int = 0
10 changes: 10 additions & 0 deletions src/main/scala/ch02/Semigroup.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package ch02

trait Semigroup[A]:
def combine(x: A, y: A): A

object SemigroupInstances:
// No identity element, can't form a Monoid.
given setIntersectionSemigroup[A]: Semigroup[Set[A]] with
def combine(a: Set[A], b: Set[A]) =
a intersect b
50 changes: 50 additions & 0 deletions src/main/scala/ch03/Codec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ch03

/*
3.5.6.1 Transformative Thinking with imap
Implement the imap method for Codec.
*/
trait Codec[A]:
def encode(value: A): String
def decode(value: String): A
def imap[B](dec: A => B, enc: B => A): Codec[B] =
val self = this
new Codec[B]:
override def encode(value: B): String =
self.encode(enc(value))

override def decode(value: String): B =
dec(self.decode(value))

def encode[A](value: A)(using c: Codec[A]): String =
c.encode(value)

def decode[A](value: String)(using c: Codec[A]): A =
c.decode(value)

object Codec:
final case class Box[A](value: A)

object CodecInstances:
given stringCodec: Codec[String] with
def encode(value: String): String = value
def decode(value: String): String = value

given Codec[Int] =
stringCodec.imap(_.toInt, _.toString)

// Demonstrate your imap method works by creating a Codec for Double.
given Codec[Double] =
stringCodec.imap(_.toDouble, _.toString)

// Implement a Codec for the Box type.
given boxCodec[A](using c: Codec[A]): Codec[Codec.Box[A]] =
c.imap[Codec.Box[A]](Codec.Box(_), _.value)

import CodecInstances.given

val ca = encode(123.4)
val cb = decode[Double]("123.4")

val cc = encode(Codec.Box(123.4))
val cd = decode[Codec.Box[Double]]("123.4")
48 changes: 48 additions & 0 deletions src/main/scala/ch03/Printable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package ch03

/*
3.5.5.1 Exercise: Showing off with Contramap
Implement the contramap method for Printable above.
*/
trait Printable[A]:
def format(value: A): String

def contramap[B](f: B => A): Printable[B] =
val self = this
new Printable[B]:
def format(value: B): String =
self.format(f(value))

def format[A](value: A)(using p: Printable[A]): String =
p.format(value)

object Printable:
final case class Box[A](value: A)

object PrintableInstances:
given Printable[String] with
def format(value: String): String =
s"'${value}'"

given Printable[Boolean] with
def format(value: Boolean): String =
if (value) "yes" else "no"

/*
Define an instance of Printable for the following Box case class.
Rather than writing out the complete definition from scratch
(new Printable[Box] etc...), create your instance from an existing
instance using contramap.
*/
// If we use `with`, method format has to be defined.
// Given a Printable[A], and a Box[A], we can create
// Printable[Box[A]] by the reverse mapping Box[A] => A.
// The forward mapping would be A => Box[A].
given [A](using p: Printable[A]): Printable[Printable.Box[A]] =
p.contramap[Printable.Box[A]](_.value)

import PrintableInstances.given

val ps = format("hello")
val pb = format(true)
val pc = format(Printable.Box("hello"))
22 changes: 22 additions & 0 deletions src/main/scala/ch03/Tree.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ch03

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]

object Tree:
given Functor[Tree] with
override def map[A, B](fa: Tree[A])(f: A => B): Tree[B] =
fa match
case Branch(l, r) => Branch(map(l)(f), map(r)(f))
case Leaf(value) => Leaf(f(value))

def branch[A](left: Tree[A], right: Tree[A]): Tree[A] =
Branch(left, right)

def leaf[A](value: A): Tree[A] =
Leaf(value)
79 changes: 79 additions & 0 deletions src/main/scala/ch03/ch03.worksheet.sc
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import cats.Functor
import cats.syntax.functor.toFunctorOps

// 3.5.2 Functor Syntax
val func1 = (a: Int) => a + 1
val func2 = (a: Int) => a * 2
val func3 = (a: Int) => s"${a}!"
val func4 = func1.map(func2).map(func3)

func4(123)

def doMath[F[_]](start: F[Int])(using functor: Functor[F]): F[Int] =
start.map(n => n + 1 * 2)

doMath(Option(20))
doMath(List(1, 2, 3))

// 3.6.1 Contravariant in Cats
import cats.Contravariant
import cats.Show
import cats.instances.string.catsStdShowForString

val showString = Show[String]

// trait Contravariant[F[_]] {
// def contramap[A, B](fa: F[A])(f: B => A): F[B]
// }

val showSymbol: Show[Symbol] = Contravariant[Show].
contramap(showString)((sym: Symbol) => s"'${sym.name}")

showSymbol.show(Symbol("dave"))

import cats.syntax.contravariant.toContravariantOps

showString
.contramap[Symbol](sym => s"'${sym.name}")
.show(Symbol("dave"))


// 3.6.2 Invariant in Cats
import cats.Monoid
import cats.instances.string.catsKernelStdMonoidForString
import cats.syntax.invariant.toInvariantOps
import cats.syntax.semigroup.catsSyntaxSemigroup

given Monoid[Symbol] =
Monoid[String].imap(Symbol.apply)(_.name)

Monoid[Symbol].empty

Symbol("a") |+| Symbol("few") |+| Symbol("words")

// 3.7 Aside: Partial Unification
val f1 = (x: Int) => x.toDouble
val f2 = (y: Double) => y * 2

val f3 = func1.map(func2)

val either: Either[String, Int] = Right(123)

either.map(_ + 1)

val f3a: Int => Double =
a => f2(f1(a))

val f3b: Int => Double =
f2.compose(f1)

// error: value contramap is not a member of Double => Double
// val f3c = f2.contramap(f1)

type <=[B, A] = A => B

type F[A] = Double <= A

val f2b: Double <= Double = f2

val f3c = f2b.contramap(f1)
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package intro.eq
package ch01.eq

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package intro.printable
package ch01.printable

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package intro.show
package ch01.show

import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
Expand Down
22 changes: 22 additions & 0 deletions src/test/scala/ch02/MonoidSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ch02
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers.shouldBe
import ch02.Lib.add

class MonoidSpec extends AnyFunSpec:
describe("Monoid"):
it("should be able to add integers"):
val ints = List(1, 2, 3)
add(ints) `shouldBe` 6

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

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

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

0 comments on commit 5e3a9b8

Please sign in to comment.