Unified Support for Numbers in Scala
If you are using Scala, you must have noticed that numeric types do not have a supertype that encapsulates common beheavior. For instance in order to support both Double’s and Float’s in your function you have to do one of the following:
// Use multiple function signatures:
def foo(bar: Double): Double = {
...
}
def foo(bar: Float): Float = {
...
}
// or use pattern matching:
def foo(bar: AnyVal): Double = {
bar match {
case barAsDouble: Double => ...
case barAsFloat: Float => ...
case _ => ...
}
}
There will be a little duplication if you need this in just one place. But your code will quickly get unpleasantly bulky if you need to this in many places.
Enter Numeric! It’s a type class[1] that provides a unified interface for all numeric types. And the cool thing is you can easily extend it. Let’s first cover usage.
Here is a very simple function that will work on Double’s, Float’s, BigDecimal’s, BigInt’s, Byte’s, Char’s, Int’s, Long’s and Short’s out of the box:
def threshold[T](value: T, limit: T)(implicit num: Numeric[T]): T = {
if(num.gt(value, limit)) {
num.one
} else {
num.zero
}
}
A couple things to note here:
- Both parameters as well as the return type are of type T. Mixing numeric types are tricky.[2]
- The implicit named num ensures you are calling this function with the right parameters, compile-time. In other words, T must be a type that supports Numeric interface. More on this later.
- I would have liked to use < instead of num.gt() but since T itself doesn’t necessarily have that method I have to use the operations on num.
- I could, however, have used integer literals 0 and 1 and then I would have had to change the return type to Int. I chose to keep the return type same so that I can combine this function with others in the consuming code. Also note that your consuming code is likely to know the concrete type so you can easily cast the result to an integer if necessary.
You know how implicit parameters work in Scala; the compiler tries to find an object of type Numeric[T] that is marked as implicit and when it finds one it will be available in your function body as num. If it cannot, there will be a compile-time error.
Let’s take a look at another simple function:
def oneOver[T](a: T)(implicit num: Fractional[T]): T = {
require(num.gt(a, num.zero))
num.div(num.one, a)
}
We can call it like this:
oneOver(9.0) + oneOver(1.5)
Note that the types are inferred when we use oneOver function, therefore we know the return types of the two calls, therefore we can use + operation.
What’s also interesting here is we have used Fractional instead of Numeric. Because Numeric doesn’t contain a division function but it’s sub-trait Fractional has. When we invoke oneOver with a Double as we did above, scala compiler looks for an object of a type compatible with Fractional[Double]. And it will find one defined in Numeric object, num will then have the value of DoubleIsFractional.
This means you can provide your own implicits that implement Numeric for a new type or you can extend/augment Numeric to teach the numeric types new tricks or you can do both. Let me know if you find interesting uses of this feature.
[1] | It’s actually a trait. For more info on type classes. |
[2] | Except converting an Int to type T, that’s easy. Just use num.fromInt(x: Int): T |
If you have any questions, suggestions or corrections feel free to drop me a line.