Table of Contents
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 theadd
method in the first example. - In the first example, the
Numeric
type class already existed, so you could just use it to create theadd
method. But when you’re starting from scratch, you need to create your own type class (the code in theHumanLike
trait). - Because a
speak
method is defined in theDogIsHumanLike
implicit object, which extendsHumanLike[Dog]
, aDog
can be passed into themakeHumanLikeThingSpeak
method. But because a similar implicit object has not been written for theCat
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’sNumeric
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
this post is sponsored by my books: | |||
#1 New Release |
FP Best Seller |
Learn Scala 3 |
Learn FP Fast |