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)
}