Type-safe Equal

Type-safe Equal

Unfortunately, Scala's equality check is not type-safe just like Java's.

e.g.) The following code can never return true as the types of "a" and 1 do not match.

Yet there is no compile-time error. Instead it compiles and returns false in runtime.

"a" == 1
// res0: Boolean = false

There are a few ways to solve this issue.

Extension methods

First one is simply using extension methods. Create a simple implicit class extending AnyVal for extension methods like the following one.

object CommonPredef {
implicit final class AnyEquals[A](val self: A) extends AnyVal {
def ===(other: A): Boolean = self == other
def !==(other: A): Boolean = self != other
}
}

Then import to use it.

import CommonPredef._
1 === 1
// res2: Boolean = true
1 !== 1
// res3: Boolean = false
1 === 2
// res4: Boolean = false
1 !== 2
// res5: Boolean = true
import CommonPredef._
// As you can see, this one has a compile-time error
"a" === 1
// error: type mismatch;
// found : Int(1)
// required: String
// "a" === 1
// ^
"a" !== 1
// error: type mismatch;
// found : Int(1)
// required: String
// "a" !== 1
// ^

Use Cats' Eq

import cats.implicits._
1 === 1
// res9: Boolean = true
1 === 2
// res10: Boolean = false
1 =!= 1
// res11: Boolean = false
1 =!= 2
// res12: Boolean = true
"a" === 1
// error: type mismatch;
// found : Int(1)
// required: String
// "a" === 1
// ^
"a" =!= 1
// error: type mismatch;
// found : Int(1)
// required: String
// "a" === 1
// ^

For case class, if you want to use equality check from the case class, use Eq.fromUniversalEquals.

e.g.)

  • Without Eq.fromUniversalEquals
import cats.implicits._
final case class Person(id: Long, name: String)
Person(1L, "Kevin") === Person(1L, "Kevin")
// error: value === is not a member of repl.Session.App2.Person
// Person(1L, "Kevin") === Person(1L, "Kevin")
// ^
// error: value === is not a member of App1.this.Person
// Person(1L, "Kevin") === Person(1L, "Kevin")
// ^^^^^^^^^^^^^^^^^^^^^^^
  • With Eq.fromUniversalEquals
import cats.kernel.Eq
import cats.implicits._
final case class Person(id: Long, name: String)
object Person {
implicit val eq: Eq[Person] = Eq.fromUniversalEquals
}
Person(1L, "Kevin") === Person(1L, "Kevin")
// res17: Boolean = true
Person(1L, "Kevin") =!= Person(1L, "Kevin")
// res18: Boolean = false

Use Scalaz's Equal

import scalaz._
import Scalaz._
1 === 1
// res20: Boolean = true
1 === 2
// res21: Boolean = false
1 /== 1
// res22: Boolean = false
1 =/= 1
// res23: Boolean = false
1 /== 2
// res24: Boolean = true
1 =/= 2
// res25: Boolean = true
"a" === 1
// error: type mismatch;
// found : Int(1)
// required: String
// "a" === 1
// ^
"a" /== 1
// error: type mismatch;
// found : Int(1)
// required: String
// "a" /== 1
// ^
"a" =/= 1
// error: type mismatch;
// found : Int(1)
// required: String
// "a" =/= 1
// ^

For case class, if you want to use equality check from the case class, use Equal.equalA.

e.g.)

  • Without Equal.equalA
import scalaz._
import Scalaz._
final case class Person(id: Long, name: String)
Person(1L, "Kevin") === Person(1L, "Kevin")
// error: value === is not a member of App6.this.Person
// Person(1L, "Kevin") === Person(1L, "Kevin")
// ^
  • With Equal.equalA
import scalaz._
import Scalaz._
final case class Person(id: Long, name: String)
object Person {
implicit val equal: Equal[Person] = Equal.equalA
}
Person(1L, "Kevin") === Person(1L, "Kevin")
// res30: Boolean = true
Person(1L, "Kevin") /== Person(1L, "Kevin")
// res31: Boolean = false
Person(1L, "Kevin") =/= Person(1L, "Kevin")
// res32: Boolean = false