Solutions to "Scala with Cats": Chapter 3

April 3, 2023

These are my solutions to the exercises of chapter 3 of Scala with Cats.

Table of Contents

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