Solutions to Scala with Cats: Chapter 1
March 25, 2023These are my solutions to the exercises of chapter 1 of Scala with Cats.
Table of Contents
- Setting Up the Scala Project
- Exercise 1.3: Printable Library
- Exercise 1.4.6: Cat Show
- Exercise 1.5.5: Equality, Liberty, and Felinity
Setting Up the Scala Project
I solved the exercises in a sandbox Scala project that has Cats as a dependency. The book recommends using a Giter8 template, so that’s what I used:
$ sbt new scalawithcats/cast-seed.g8
The above command generates (at the time of writing) a minimal project with the
following build.sbt
file:
name := "scala-with-cats"
version := "0.0.1-SNAPSHOT"
scalaVersion := "2.13.8"
libraryDependencies += "org.typelevel" %% "cats-core" % "2.8.0"
// scalac options come from the sbt-tpolecat plugin so need to set any here
addCompilerPlugin("org.typelevel" %% "kind-projector" % "0.13.2" cross CrossVersion.full)
The above differs a bit from what the book lists, since there are both new Scala 2.13 and Cats versions out already, but I followed along using these settings with minimal issues.
Exercise 1.3: Printable Library
The definition of the Printable
type class can be as follows:
trait Printable[A] {
def format(value: A): String
}
In terms of defining the Printable
instances for Scala types, I’d probably
prefer to include those in the companion object of Printable
so that they were
readily available in the implicit scope, but the exercise asks us explicitly to
create a PrintableInstances
object:
object PrintableInstances {
implicit val stringPrintable: Printable[String] =
new Printable[String] {
def format(value: String): String = value
}
implicit val intPrintable: Printable[Int] =
new Printable[Int] {
def format(value: Int): String = value.toString
}
}
The interface methods in the companion object of Printable
can be defined as
follows:
object Printable {
def format[A](value: A)(implicit p: Printable[A]): String =
p.format(value)
def print[A](value: A)(implicit p: Printable[A]): Unit =
println(p.format(value))
}
On the above, the print
method could have relied on the format
method
directly, but I opted to not have the unnecessary call.
For the Cat
example, we can define a Printable
instance for that data type
directly in its companion object:
final case class Cat(name: String, age: Int, color: String)
object Cat {
implicit val printable: Printable[Cat] =
new Printable[Cat] {
import PrintableInstances._
val sp = implicitly[Printable[String]]
val ip = implicitly[Printable[Int]]
def format(value: Cat): String = {
val name = sp.format(value.name)
val age = ip.format(value.age)
val color = sp.format(value.color)
s"$name is a $age year-old $color cat."
}
}
}
This allows us to use the Printable
instance without explicit imports:
val garfield = Cat("Garfield", 41, "ginger and black")
Printable.print(garfield)
// Prints "Garfield is a 41 year-old ginger and black cat.".
For the extension methods, we can define the PrintableSyntax
object as
follows:
object PrintableSyntax {
implicit class PrintableOps[A](val value: A) extends AnyVal {
def format(implicit p: Printable[A]): String =
p.format(value)
def print(implicit p: Printable[A]): Unit =
println(p.format(value))
}
}
I have opted to use a value class for performance reasons, but for the purpose of this exercise it was likely unnecessary.
By importing PrintableSyntax._
we can now call print
directly on our Cat
instance:
import PrintableSyntax.__
val garfield = Cat("Garfield", 41, "ginger and black")
garfield.print
// Prints "Garfield is a 41 year-old ginger and black cat.".
Exercise 1.4.6: Cat Show
To implement the previous example using Show
instead of Printable
, we need
to define an instance of Show
for Cat
. Similar to the approach taken before,
we’re defining the instance directly in the companion object of Cat
:
import cats.Show
final case class Cat(name: String, age: Int, color: String)
object Cat {
implicit val show: Show[Cat] =
new Show[Cat] {
val stringShow = Show[String]
val intShow = Show[Int]
def show(t: Cat): String = {
val name = stringShow.show(t.name)
val age = intShow.show(t.age)
val color = stringShow.show(t.color)
s"$name is a $age year-old $color cat."
}
}
}
Cats implements summoners for the Show
type class, so we no longer need to use
implicitly
.
This can be used as follows:
import cats.implicits._
val garfield = Cat("Garfield", 41, "ginger and black")
println(garfield.show)
// Prints "Garfield is a 41 year-old ginger and black cat.".
Cats doesn’t have an extension method to directly print an instance using its
Show
instance, so we’re using println
with the value returned by the show
call.
Exercise 1.5.5: Equality, Liberty, and Felinity
A possible Eq
instance for Cat
can be implemented as follows. Similar to the
above, I’ve opted to include it in the companion object of Cat
.
object Cat {
implicit val eq: Eq[Cat] =
new Eq[Cat] {
val stringEq = Eq[String]
val intEq = Eq[Int]
def eqv(x: Cat, y: Cat): Boolean =
stringEq.eqv(x.name, y.name) && intEq.eqv(x.age, y.age) && stringEq.eqv(x.color, y.color)
}
}
We can now use it to compare Cat
instances:
import cats.implicits._
val cat1 = Cat("Garfield", 38, "orange and black")
val cat2 = Cat("Heathcliff", 33, "orange and black")
val optionCat1 = Option(cat1)
val optionCat2 = Option.empty[Cat]
cat1 === cat2
// Returns false.
cat1 === cat1
// Returns true.
optionCat1 === optionCat2
// Returns false.