Solutions to Scala with Cats: Chapter 3
April 3, 2023These are my solutions to the exercises of chapter 3 of Scala with Cats.
Table of Contents
- Exercise 3.5.4: Branching out with Functors
- Exercise 3.6.1.1: Showing off with Contramap
- Exercise 3.6.2.1: Transformative Thinking with imap
Exercise 3.5.4: Branching out with Functors
A Functor for Tree can be implemented as follows:
import cats.Functor
implicit val treeFunctor: Functor[Tree] = new Functor[Tree] {
def map[A, B](fa: Tree[A])(f: A => B): Tree[B] =
fa match {
case Branch(left, right) =>
Branch(map(left)(f), map(right)(f))
case Leaf(value) =>
Leaf(f(value))
}
}Note that the implementation above is not stack-safe, but I didn’t worry to much
about it. We can check that the implementation works as expected by using map
over some Tree instances:
import cats.syntax.functor._
val tree: Tree[Int] = Branch(Branch(Leaf(1), Leaf(2)), Branch(Leaf(3), Leaf(4)))
tree.map(_ * 2)
// Returns Branch(Branch(Leaf(2),Leaf(4)),Branch(Leaf(6),Leaf(8))).
tree.map(_.toString)
// Returns Branch(Branch(Leaf("1"),Leaf("2")),Branch(Leaf("3"),Leaf("4"))).On the above, we won’t be able to call map directly over instances of Branch
or Leaf because we don’t have Functor instances in place for those types. To
make the API more friendly, we can add smart constructors to Tree (i.e.
branch and leaf methods that return instances of type Tree).
Exercise 3.6.1.1: Showing off with Contramap
To implement the contramap method, we can create a Printable instance that
uses the format of the instance it’s called on (note the self reference) and
uses func to transform the value to an appropriate type:
trait Printable[A] { self =>
def format(value: A): String
def contramap[B](func: B => A): Printable[B] =
new Printable[B] {
def format(value: B): String =
self.format(func(value))
}
}With this contramap method in place, it becomes simpler to define a
Printable instance for our Box case class:
final case class Box[A](value: A)
object Box {
implicit def printableBox[A](implicit p: Printable[A]): Printable[Box[A]] =
p.contramap(_.value)
}Exercise 3.6.2.1: Transformative Thinking with imap
To implement imap for Codec, we need to rely on the encode and decode
methods of the instance imap is called on:
trait Codec[A] { self =>
def encode(value: A): String
def decode(value: String): A
def imap[B](dec: A => B, enc: B => A): Codec[B] =
new Codec[B] {
def encode(value: B): String = self.encode(enc(value))
def decode(value: String): B = dec(self.decode(value))
}
}Similarly to what’s described in the chapter, we can create a Codec for
Double by piggybacking on the Codec for String that we already have in
place:
implicit val doubleCodec: Codec[Double] =
stringCodec.imap(_.toDouble, _.toString)When implementing the Codec for Box, we can use imap and describe how to
box and unbox a value, respectively:
final case class Box[A](value: A)
object Box {
implicit def codec[A](implicit c: Codec[A]): Codec[Box[A]] =
c.imap(Box.apply, _.value)
}