How to add new behavior to closed classes in Scala (type classes)

This is an excerpt from the Scala Cookbook (partially modified for the internet). This is Recipe 19.7, “How to add new behavior to closed classes (models) in Scala (type classes).”

Problem

You have a closed model, and want to add new behavior to certain types within that model, while potentially excluding that behavior from being added to other types.

Solution

Implement your solution as a type class.

To demonstrate the problem and solution, when I first came to Scala, I thought it would be easy to write a single add method that would add any two numeric parameters, regardless of whether they were an Int, Double, Float, or other numeric value. Unfortunately I couldn’t get this to work — until I learned about type classes.

Because a Numeric type class already exists in the Scala library, it turns out that you can create an add method that accepts different numeric types like this:

def add[A](x: A, y: A)(implicit numeric: Numeric[A]): A = numeric.plus(x, y)

Once defined, this method can be used with different numeric types like this:

println(add(1, 1))
println(add(1.0, 1.5))
println(add(1, 1.5F))

The add method works because of some magic in the scala.math.Numeric trait. To see how this magic works, create your own type class.

Creating a type class

The process of creating a type class is a little complicated, but there is a formula:

  • Usually you start with a need, such as having a closed model to which you want to add new behavior.
  • To add the new behavior, you define a type class. The typical approach is to create a base trait, and then write specific implementations of that trait using implicit objects.
  • Back in your main application, create a method that uses the type class to apply the behavior to the closed model, such as writing the add method in the previous example.

To demonstrate this, assume that you have a closed model that contains Dog and Cat types, and you want to make a Dog more human-like by giving it the capability to speak. However, while doing this, you don’t want to make a Cat more human-like. (Everyone knows that dogs are human-like and can speak; see YouTube for examples.)

The closed model is defined in a class named Animals.scala, and looks like this:

package typeclassdemo

// an existing, closed model
trait Animal
final case class Dog(name: String) extends Animal
final case class Cat(name: String) extends Animal

To begin making a new speak behavior available to a Dog, create a type class that implements the speak behavior for a Dog, but not a Cat:

package typeclassdemo

object Humanish {

    // the type class.
    // defines an abstract method named 'speak'.
    trait HumanLike[A] {
        def speak(speaker: A): Unit
    }

    // companion object
    object HumanLike {
        // implement the behavior for each desired type. in this case,
        // only for a Dog.
        implicit object DogIsHumanLike extends HumanLike[Dog] {
            def speak(dog: Dog) { println(s"I'm a Dog, my name is ${dog.name}") }
        }
    }
}

With this behavior defined, use the new functionality back in your main application:

package typeclassdemo

object TypeClassDemo extends App {
    import Humanish.HumanLike

    // create a method to make an animal speak
    def makeHumanLikeThingSpeak[A](animal: A)(implicit humanLike: HumanLike[A]) {
      humanLike.speak(animal)
    }

    // because HumanLike implemented this for a Dog, it will work
    makeHumanLikeThingSpeak(Dog("Rover"))

    // however, the method won't compile for a Cat (as desired)
    //makeHumanLikeThingSpeak(Cat("Morris"))
}

The comments in the code explain why this approach works for a Dog, but not a Cat.

There are a few other things to notice from this code:

  • The makeHumanLikeThingSpeak is similar to the add method in the first example.
  • In the first example, the Numeric type class already existed, so you could just use it to create the add method. But when you’re starting from scratch, you need to create your own type class (the code in the HumanLike trait).
  • Because a speak method is defined in the DogIsHumanLike implicit object, which extends HumanLike[Dog], a Dog can be passed into the makeHumanLikeThingSpeak method. But because a similar implicit object has not been written for the Cat class, it can’t be used.

Discussion

Despite the name “class,” a type class doesn’t come from the OOP world; it comes from the FP world, specifically Haskell. As shown in the examples, one benefit of a type class is that you can add behavior to a closed model.

Another benefit is that it lets you define methods that take generic types, and provide control over what those types are. For instance, in the first example, the add method takes Numeric types:

def add[A](x: A, y: A)(implicit numeric: Numeric[A]): A = numeric.plus(x, y)

Because the numeric.plus method is implemented for all the different numeric types, you can create an add method that works for Int, Double, Float, and other types:

println(add(1, 1))
println(add(1.0, 1.5))
println(add(1, 1.5F))

This is great; it works for all numeric types, as desired. As an additional benefit, the add method is type safe. If you attempted to pass a String into it, it won’t compile:

// won't compile
add("1", 2.0)

In the second example, the makeHumanLikeThingSpeak method is similar to the add method. However, in this case, it lets a Dog type speak, but because the HumanLike trait didn’t define a similar behavior for a Cat, a Cat instance can’t currently be used by the method. You can resolve this by adding a speak method for a Cat type as another implicit object, or keep the code as it’s currently written to prevent a Cat from speaking.

See Also

  • If you dig into the source code for Scala’s Numeric trait, you’ll find that it’s implemented in a manner similar to what’s shown here. You can find the source code for Scala’s Numeric trait by following the “Source code” link on its Scaladoc page
  • Recipe 1.10, “Add Your Own Methods to the String Class” demonstrates how to add new functionality to closed classes using implicit conversions