Monday, October 19, 2015

Ad-Hoc Polymorphism and Typeclasses in Scala, part 2

In part 1, we covered the types of polymorphism and how ad-hoc polymorphism works in Haskell. Now, we'll look at implementing it in Scala. We'll start with a basic example and build up to a better solution.

We'll implement an Equal typeclass (Eq in Haskell) that is used for comparing 2 values of the same type. To do this, we'll create a trait and an implementation of it for Ints.

trait Equal[A] {
  def equal(a1: A, a2: A): Boolean
}

class EqualInt extends Equal[Int] {
  def equal(a1: Int, a2: Int): Boolean = a1 == a2
}

We'll also make a function that uses it:

def areTheyEqual[A](a1: A, a2: A, eq: Equal[A]): String = {
  if (eq(a1, a2)) "Yes"
  else "No"
}

OK. It's a lame function, but it should serve our purposes for now. We can call the function like:

val areThey = areTheyEqual(1, 3, new EqualInt)

The function can now compare different types, as long as we pass an implementation for Equal for that type to it. But, it's a far cry from elegant. The next step is to make the Equal parameter implicit. This requires making another parameter group:

def areTheyEqual[A](a1: A, a2: A)(implicit eq: Equal): String ...

We can also define areTheyEqual() using context bound syntax like:

def areTheyEqual[A: Equal](a1: A, a2: A) = {
  if (implicitly[Equal[A]]equal(a1, a2) "Yes"
  else "No"
}

Either way, we can call it like:

val areThey = areTheyEqual(1, 3)(new EqualInt)

But, we can also pass the parameter implicitly if an appropriate value is in scope:

implicit val eq = new EqualInt
val areThey = areTheyEqual(1, 3)

The Scala compiler sees that the implicit parameter is missing, and goes looking for one for us. However, at this point we still need to create the missing implicit value. To make this easier, we'll put some default implicit values in the companion object for the Equal trait. If the compiler doesn't find an implicit value in the local scope or that is explicitly imported it will check in the companion object. This provides a convenient place to put the default implicit values.

object Equal {
  implicit object EqualInt extends Equal[Int] {
    def equal(a1: Int, a2: Int): Boolean = a == b
}

While the above works in almost all cases, it is better to explicitly give the type of implicit value to avoid some subtle type errors that can occur. You can do that like this (as well as adding an instance for Strings):

object Equal {
  implicit val equalInt: Equal[Int] = new Equal[Int] {
    def equal(a1: Int, a2: Int): Boolean = a == b
  }
  implicit val equalString: Equal[String] = new Equal[String] {
    def equal(a1: String, a2: String): Boolean = a == b
  }
}

The definitions for Int and String are identical here, but that is not the case in general. Now we don't have to create our own implicit val eq = new EqualInt, and we can also use areTheyEqual() for Strings:

val areThey1 = areTheyEqual(1, 3)
val areThey2 = areTheyEqual("xyz", "xyz")

Calling areTheyEqual() for different types is now simple and clean. As a library developer for Equal, you would want to provide a number of default implementations for different types. Users could then make their types "instances" of the Equal "typeclass" in the same way, without needing to alter their type at all.

But, what if you have a need to compare Strings by a different criteria - maybe case insensitive, or you only care about the length of the strings. You can create your own implicit instance, either in the local scope or in a package you explicitly import, and it will take precedence over the one defined in the Equal package:

implicit val equalStrLen: Equal[String] = new Equal[String] {
  def equal(a1: String, a2: String): Boolean = a1.length = a2.length
}

val areThey = areTheyEqual("abc", "xyz") // now is true

Of course, you can also still pass an instance of Equal explicitly:

val areThey = areTheyEqual("abc", "xyz")(equalStrLen)

Ok. That's great. But, one thing is still bothering me. I have to do my comparison like so:

  eq.equal(1, 3)

It would be much nicer to be able to call equal on one of the parameters, like 1.equal(3), or even use something that looks more like an operator: 1 === 3.

So far, we've used the implicit mechanism for passing an implicit parameter. But, there is another way it can be used - for implicitly converting one type to another. This is how Scala allows methods to be called on values like Int, which are not present in the Java implementation of them, while still being compatible with Java. When the Scala compiler sees a method being called on an object, but doesn't find that method defined on the object, it looks around for another class that DOES have that method, and for an implicit method that will convert the original type to that type. In the case of Int, if you type 5 min 7, the compiler sees Int does not have a min method, but it finds an implicit method that converts an Int to a RichInt, which does have that method, so it performs the conversion.

That is essentially what we want to do here, but we want to do it in a more general fashion. So, we'll start by creating a trait that can wrap any kind of value, and an implicit function for doing the conversion:

trait Identity[A] {
 val value: A
}

implicit def toIdentity[A](a: A): Identity[A] = new Identity[A] {val value = a}


So, now we can implicitly convert any type A to an Identity[A], but it doesn't do anything. To remedy this, we can add method definitions to the trait:

trait Identity[A] {
  val value: A

  def ===(a2: A)(implicit eq: Equal[A]) = eq.equal(value, a2)
  def =/=(a2: A)implicit eq: Equal[A]) = !eq.equal(value, a2)
}

The compiler will now allow us to call the === method on any type that can be converted to Identity (which is every type) AND for which there is an implicit Equal[A] available. Now we can use our trait in a much more natural fashion:

1 === 2
"abc" =/= "xyz"

I know what you're thinking: "Great! He just went to a lot of trouble to implement ==, and !=". But, there is one big difference. Equal is type safe. In the Scala REPL, if you use == on 2 different types, you'll see something like this:


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

It emits a warning, but does the comparison anyway. Let's try it with ===.

scala> 1 === "one"
<console>:15: error: type mismatch;
 found   : String("one")
 required: Int
       1 === "one"
             ^
You now get a compiler error if you try to compare dissimilar types.

We can also use the Equal typeclass in other typeclasses. In part 3, we will extend our example.

No comments:

Post a Comment