Scala Implicits

Jan 29, 2022
#cs


Implicits are a feature of the Scala programming language that I have not seen in other languages before. The feature allows the Scala compiler to implicitly perform actions in certain situations, without you as the programmer needing to write the action explicitly. There are three different ways of using implicits in Scala: implicit values/parameters, implicit classes, and implicit conversions.

Implicit Values/Parameters

Implicit parameters let you pass parameters implicitly into methods. Consider the following example:

def addOne(implicit num: Int): Int = num + 1

Ignoring the implicit keyword for now, this method accepts an integer and returns one plus that integer:

$ def addOne(implicit num: Int): Int = num + 1
defined function addOne

$ addOne(5)
res1: Int = 6

However, because we mark the num parameter as implicit, we can omit the parameter when we call this method. If we do so, the compiler will look for a value defined using the implicit keyword with the desired type (in this case, Int) to use as the parameter:

$ def addOne(implicit num: Int): Int = num + 1
defined function addOne

$ implicit val x: Int = 10
x: Int = 10

$ addOne
res3: Int = 11

As you can see in this example, when we omit the parameter, the Scala compiler implicitly uses 10 as the value for num. If you're wondering where the compiler looks to find these implicit values, this Stack Overflow answer explains it in detail. At a high level, Scala first looks through the current scope, then the imports, and finally the companion object of the type.

What happens if the method is called without arguments, and no implicit values are in scope?

$ def addOne(implicit num: Int): Int = num + 1
defined function addOne

$ addOne
cmd1.sc:1: could not find implicit value for parameter num: Int
val res1 = addOne
           ^
Compilation Failed

In this case, the code fails to compile, since the compiler cannot find any implicit integers in scope. This demonstrates one nice property of implicits, which is that if an implicit value with the desired type is not available, then the code fails at compile time, not runtime.

Implicit values/parameters are useful because they allow us to express the idea that some value should be used by default in most situations, but can be overridden if needed. In our toy example, our method accepted an implicit integer, but in practice it's unlikely that we'd write methods that accept an implicit integer, since there is no integer that is used by default in most situations. On the other hand, one practical application of implicit values/parameters is sorting. Consider the type signature of the sorted method on Scala's Array class:

def sorted[B >: A](implicit ord: math.Ordering[B]): Array[T]

Note that the method accepts an implicit Ordering object. The Ordering object implements a comparison method that takes in two values and returns which one should be considered "smaller". Scala already comes with implicit Ordering objects defined for many basic types. For example, the default implicit Ordering[Int] object for integers orders them from smallest to largest (source). This means if we call the sorted method on an Array of integers, it will sort the array from smallest to largest:

$ val arr = Array(5,3,4,1,2)
arr: Array[Int] = Array(5, 3, 4, 1, 2)

$ arr.sorted
res2: Array[Int] = Array(1, 2, 3, 4, 5)

If we want to instead sort from to largest to smallest, we could construct our own Ordering object and pass that in to the sorted method:

$ val arr = Array(5,3,4,1,2)
arr: Array[Int] = Array(5, 3, 4, 1, 2)

$ object LargeToSmall extends math.Ordering[Int] {
  def compare(x: Int, y: Int) = -java.lang.Integer.compare(x, y)
}
defined object LargeToSmall

$ arr.sorted(LargeToSmall)
res4: Array[Int] = Array(5, 4, 3, 2, 1)

Taking this a step further, we can declare the LargeToSmall object to be implicit:

$ val arr = Array(5,3,4,1,2)

$ implicit object LargeToSmall extends math.Ordering[Int] {
  def compare(x: Int, y: Int) = -java.lang.Integer.compare(x, y)
}
defined object LargeToSmall

$ arr.sorted
res2: Array[Int] = Array(5, 4, 3, 2, 1)

Now, you can see that we didn't have to explicitly pass the Ordering object into the sort method. The advantage of the implicit approach is that other methods that accepts an implicit Ordering will automatically use the new implicit object we defined as well, without us having to pass the Ordering object in to every one:

$ arr.max // will return the smallest number because it uses our implicit ordering
res3: Int = 1

Implicit Classes

Implicit classes allow us to add functionality (i.e. methods) to existing classes. For example, lets say we want to add a method numVowels to the String class. In Scala, we cannot modify the original class directly, but we can accomplish this using implicit classes:

$ implicit class StringWrapper(val s: String) {
  def numVowels = s.toLowerCase.count(c => Set('a', 'e', 'i', 'o', 'u').contains(c))
}
defined class StringWrapper

$ val testString = "Hello, World!"
testString: String = "Hello, World!"

$ testString.numVowels
res8: Int = 3

The way this works is that when the Scala compiler encounters testString.numVowels, it realizes that the String class does not have a numVowels method, so it checks to see if there are any implicit classes that wrap a String object, and define a numVowels method. It finds StringWrapper, so it uses that class's implementation of numVowels. The Scala compiler is implicitly replacing testString.numVowels with StringWrapper(testString).numVowels for us. Furthermore, once we've defined this implicit class, we're able to call the numVowels method on any String when the class is in scope.

Implicit Conversions

Implicit conversions allow us to automatically convert values from one type to another. Defining an implicit conversion just involves declaring an implicit method that accepts a value of type A as input and outputs a value of type B. Then, whenever the compiler encounters a value of type A but expects a value of type B, it will implicitly call this method to do the conversion.

For example, the boolean values true and false are often represented as 1 and 0. We can write an implicit conversion that allows us to automatically use booleans as integers:

$ implicit def boolToInt(b: Boolean): Int = if (b) 1 else 0
defined function boolToInt

$ 5 + true
res15: Int = 6

Implicit conversions are useful because they allow us to express the idea that there is a canonical way to interpret a value of one type as another.

Summary

Implicit Type What We Write (implicit) What the Compiler Does (explicit)
Value/Parameter addOne addOne(x)
Class testString.numVowels StringWrapper(testString).numVowels
Conversion 5 + true 5 + boolToInt(true)

As we have seen, Scala's implicits feature allows us to make the compiler implicitly use values, classes, and methods in certain scenarios, instead of having to explicitly write them out. It's only possible for the Scala compiler to infer what implicit to use because Scala has a very rich type system. Normally, strictly typed languages are associated with being more verbose, but implicits show that having types can actually help make our code more concise.





Comment