A very simple look at how to pimp a method onto a class using Scala's implicits.
I recently enjoyed a couple of days with Heiko Seeberger and Josh Suereth as they dragged me through some Advanced Scala Training and it was quite awesome. As an exercise, to test out my new found skills I pimped a new method onto “numbers” in Scala and I thought it might be instructive to share.
The Goal
The goal is really quite simple. All we want to do is add a new method, squared
onto “numbers”. I don’t mean Int
directly, but anything that Scala considers a number. We should see things like this:
5.squared // == 25
25.0.squared // == 625.0
The Code
The code is wonderfully simple for this.
implicit def numeric2Powerable[A : Numeric](i: A) = new {
def squared: A = implicitly[Numeric[A]].times(i, i)
}
Done. What we’ve managed to do in such a small amount of code is quite surprising. Let’s break it down.
implicit
: Theimplicit
puts the conversion definition into the local scope so that we can get a transparent conversion from our number to our pimped object.[A : Numeric]
: This is a context bound. It states that the type parameter we’re going to constrain this implicit conversion to must have an instance of the Numeric type class in scope. It’s this constraint that limits the types we’re pimping down to being numbers.implicitly[...]
: Because we’ve chosen the “clean” declaration for the context bound (as opposed to the slightly uglier curried parameter version) the instance of the type class has not been bound to an identifier that we can later use. In order to get access to the instance, we must “look it up”. Because scala.Predef is always imported by default, and becauseimplicitly
is defined there, we have access to it, and this is what we use to resolve the implicit instance.
The reset is self-explanatory.
That’s all there is to it. We’ve defined an implicit conversion from a number to a new pimp-class, used a type class to narrow the target types for the pimp, and pulled in the instance of the type class to give us a concrete tool to use in performing the computation. Not bad for a couple of lines of code.
Addendum: Missingfaktor showed me another little fun bit that you can add on to the code above to make the implementation feel more natural. We can import the implicits that are defined on Numeric to give us some infix notation instead.
implicit def numeric2Powerable[A : Numeric](i: A) = new {
import Numeric.Implicits._
def squared: A = i * i
}
It doesn’t change the API at all, but it does look a little niftier with respect to the implementation. It’s a little less simple, from an instructive point of view, because you start to wonder where the implicitly
wandered away to. So, some more explanation is probably in order.
Specifically, what we’re importing is:
object Numeric {
trait ExtraImplicits {
implicit def infixNumericOps[T](x: T)(implicit num: Numeric[T]): Numeric[T]#Ops = new num.Ops(x)
}
object Implicits extends ExtraImplicits { }
}
So, it’s really the infixNumericOps
that we’re pulling in, and it’s delegating to Ops
. So, what’s Ops
?
Ops
is defined as an inner class to the Numeric
type trait and looks like:
class Ops(lhs: T) {
def +(rhs: T) = plus(lhs, rhs)
def -(rhs: T) = minus(lhs, rhs)
def *(rhs: T) = times(lhs, rhs)
def unary_-() = negate(lhs)
def abs(): T = Numeric.this.abs(lhs)
def signum(): Int = Numeric.this.signum(lhs)
def toInt(): Int = Numeric.this.toInt(lhs)
def toLong(): Long = Numeric.this.toLong(lhs)
def toFloat(): Float = Numeric.this.toFloat(lhs)
def toDouble(): Double = Numeric.this.toDouble(lhs)
}
So, when we stitch this together, here’s what happens:
- We bring
infixNumericOps
into scope. - We then invoke
i * i
, which is equivalent toi.*(i)
. - The
*
can’t be found oni
sincei
is of typeA
. - However, now that we have
infixNumericOps
in scope, the compiler can use that conversion to create an instance ofNumeric[A]#Ops
. Numeric[A]#Ops
does have*
defined for typeA
, thus, a newOps
is constructued, holding our valuei
within it.- The compiler then applies the
*
operator to to the second value ofi
and we’re done.
Cooler, but a hell of a lot more complicated to explain :)