Implicit Classes, i.e. Extensions
Series part 2, from “Scala and the Tree Implicits”
In the first article of this three-part series, we learned that Scala 2.x used this keyword “implicit” that was used in three distinct (and slightly confusing) ways. For anyone wanting to jump straight into Scala 3, the word “implicit” gets morphed into different terms—don’t worry, we’ll cover how the new syntax works in each article.
We covered implicit conversion first, mostly because it’s the simplest “magic trick” and the easiest for a scala beginner to learn, but it’s also ironically the least useful and is in fact so prone to causing problems that the creators decided that it should be intentionally obfuscated.
The second of the “three sisters” is the exact opposite: it’s incredibly powerful and useful and can make your code so seemingly magical things. It is a fundamental part of what’s called the “pimp my library” design pattern. It’s also remarkably easy to use.
Case study: Pairwise adding lists together
Let’s start with a real-world example, and we’ll build upon this across the rest of the series. As a data engineer and periodic Spark user, I often work with records of k-dimensional vectors, essentially k-dimensional lists of numbers, and I like to be able to do basic mathematical operations with them. I.e. I often find myself wanting to do something like:
List(1.0, 1.2) + List(3.2, 3.2) = List(4.2, 4.4)
If I try to do this in Scala, I’ll get an immediate error, which is itself confusing since we don’t know why Scala is trying to turn this into string manipulation.
scala> List(1.0, 1.2) + List(3.2, 3.2)
error: type mismatch;
found : List[Double]
required: String
Now talking about lists generally, not as a mathematician, it would be more natural to think of addition of lists to be equivalent to concatenation, i.e.
List(1.0, 1.2) ++ List(3.2, 3.2) = List(1.0, 1.2, 3.2, 3.2)
(And yes, this works in the Scala console.) But I really would like to enable basic vector operations, and I want to make it look elegant. In Object Oriented programming, the solution would be to something like subtype List with something like MathList
and then to put the new logic in the subtype, but subtyping can get messy and confusing. Can’t we do something cleaner?
In a nutshell, yes. Scala allows us to take a pre-existing class (like List) bolt on functionality, and we do this with what’s called an implicit class.
implicit class Ops(list: List[Double]) {
def +(other: List[Double]): List[Double] = {
(list zip other).map(t => t._1 + t._2)
}
}
Bam! All you have to do is bring this implicit class into scope with an import statement, and the “+” method is defined! What the Scala compiler is doing in the background is automatically creating an Ops class wrapper around any List[Double]
object it sees and allowing the defined methods to be accessed.
In other words, if there’s anything you want to add functionality to, whether it’s part of the Scala language (like “String”) or something that came from a 3rd-party proprietary library, you can bolt functionality on top of it.
Let’s look at another simple example where this might be elegant. Let’s say we have a Coordinate case class that represents latitude/longitude coordinates. We know that often a user might have a simple (Double, Double)
tuple that he or she would like to conveniently call a .toCoordinate
method on to convert into our case class. This would simply be done with:
case class Coordinate(latitude: Double, longitude: Double)object Coordinate {
implicit class Ops(latLong: (Double, Double)) {
def toCoordinate: Coordinate = new Coordinate(latLong._1,
latLong._2)
}
}
Once in scope, you can then simply to do this to an ordinary tuple of doubles:
scala> (234.2, 4225.8).toCoordinate
val res1: Coordinate = Coordinate(234.2,4225.8)
An honestly, you could just skip the case class and a bunch of coordinate-based methods, like converting degrees to radians or back, to any (Double, Double)
tuples. That’s pretty magical, but I would keep the case class around because it doesn’t add much baggage and it makes it easier to use the type system to help document that you are, in fact, working with Coordinates. (I have a short article that talks about that point here.)
Doing this in Scala 3
Since you’re really “extending” a class with some new functionality, the Scala designers decided to make the syntax more intuitive by creating extension methods. The way you would handle the List pairwise adding example would simply leverage the extension
keyword like this:
extension (list: List[Double])
def +(other: List[Double]): List[Double] =
(list zip other).map(t => t._1 + t._2)
And the coordinate example would be written:
extension (latLong: (Double, Double))
def toCoordinate: Coordinate = new Coordinate(latLong._1,
latLong._2)
Coming up next…
Next we’ll talk about the last of the “Three Implicits”, implicit parameters, which you could sort of think of as a kind of dependency injection, but it’s really powerful when you combine it with type classes. But first, we’ll start the article with a brief demonstration of something called currying.