Taming Cats - Equality

Cats is a library which provides abstractions for functional programming in the Scala programming language. The name is a playful shortening of the word category.

Cats is a huge library that offers a lot of value. Instead of listing all its features, I will look into Scala’s shortcomings and see how Cats can help. Hopefully, you will learn a few things along the way and maybe include Cats in your next projects.

This time, I will look at equality.

Scala is a strongly typed language. In short, the compiler, scalac, checks the code for errors. The compilation will fail if any issue is found.

A common compilation error is a type mismatch.

scala> val foo: Int = "1"
<console>:11: error: type mismatch;
 found   : String("1")
 required: Int
       val foo: Int = "1"

The compiler highlights a variable assignment that can’t work. It requires a particular type, Int in our example, but finds another, String. The error message guides us to solve the issue.

The compiler enforces type safety in most places, but equality isn’t one. If we compare an Int to a String, the compiler will allow it and return false. A warning is sometimes displayed.

scala> 1 == "1"
<console>:12: warning: comparing values of types Int and String using `==' will always yield false
       1 == "1"
         ^

scala> "1" == 1
res0: Boolean = false

The code is valid as == is defined in the Any class. It compares one Any to another. It compiles, but I doubt anyone would intentionally want a comparator that always returns false.

A more restrictive equality statement would help avoid errors.

scala> def ===[T](a: T, b: T): Boolean = a == b
$eq$eq$eq: [T](a: T, b: T)Boolean

scala> ===[Int](1, 1)
res0: Boolean = true

scala> ===[Int](1, "1")
<console>:13: error: type mismatch;
 found   : String("1")
 required: Int
       ===[Int](1, "1")

Our === method only compares variables of type T. The compilation fails if the types don’t match. This version requires an explicit value for T otherwise, type inference will use Any.

scala> ===(1, "1")
res2: Boolean = false

No one wants to specify T and omitting it doesn’t solve the issue. A proper solution must infer T only from one of the variables.

scala> class Eq[T](a: T) {
     |   def ===(b: T) = a == b
     | }
defined class Eq

scala> new Eq(1) === 1
res0: Boolean = true

scala> new Eq(1) === "1"
<console>:13: error: type mismatch;
 found   : String("1")
 required: Int
       new Eq(1) === "1"

Our Eq class works without the need to specify T, but you must create an instance. Scala’s implicit conversion can do that for us.

scala> implicit class Eq[T](a: T) {
     |   def ===(b: T) = a == b
     | }
defined class Eq

scala> 1 === 1
res0: Boolean = true

scala> 1 === "1"
<console>:13: error: type mismatch;
 found   : String("1")
 required: Int
       1 === "1"

The above works great in the Scala REPL but requires a bit more work to be used in a Scala project. The implicit class must be defined in an object and imported before === can be used.

// src/main/scala/Main.scala
object EqSyntax {
  implicit class Eq[T](a: T) {
    def ===(b: T) = a == b
  }
}

import EqSyntax._

object Main extends App {
  1 === 1
}

All it takes is 5 lines of code and an import statement to safely compare two variables. It solves the unsafe ==, but using it in production requires more work, tests, releases, …

Instead of building and maintaining a full blow project, use Cats’ Eq. It offers a much more flexible API and greatly reduces the risk of always false equality statements.

import cats.implicits._
assert(1 === 1)