Understanding Implicit Parameters (with a Primer on Currying)
Third of the Implicit Sisters, but not the end of the story.
This is the third part of a four-part series. If you want to read about all the three forms of “implicit”, you might want to start here. But if you just want to learn about implicit parameters (and their Scala 3 counterparts that introduce the given
, using
, and summon
counterparts), read on!
But first I want to pause and explain some interesting syntax that is likely to confuse newcomers to Scala.
A Quick Primer on Currying
For this article, I’m going to use a slightly contrived example of some methods that work with numbers, needing to convert them into properly formatted strings. These functions will include makeString
for simply converting a number into a string, reportError
for reporting that a value is illegal, and printDouble
for printing twice the value of a given value.
import java.text.NumberFormatdef makeString(n: Double, f: NumberFormat): String = f.format(n)def reportError(n: Double, f: NumberFormat): String =
makeString(n,f) + " isn't allowed here."def printDouble(n: Double, f: NumberFormat): String =
makeString(n * 2,f)
All of these functions take two inputs (a number and a NumberFormat
object) and return a string. Scala has an interesting feature whereby if you have only one of the two inputs, you can supply it and convert a function with two “holes” into a function with only one “hole”. We do this by writing the inputs in separate sections like this:
def makeString(n: Double)(f: NumberFormat): String = f.format(n)
If I have both of the inputs, I can supply them to the function like this:
scala> makeString(2.34)(NumberFormat.getPercentInstance)
val res0: String = 234%
But if I have only the first value, I can supply that and indicate that I still have a hole in the second parameter, and what gets returned is a new function that is meant to be used later when the number format is available, like this:
scala> val twoThreeFour = makeString(2.34)(_)val twoThreeFour: java.text.NumberFormat => String = $Lambda$1398/0x00000008011462d8@10d56aabscala> twoThreeFour(NumberFormat.getCurrencyInstance)val res1: String = $2.34
This example isn’t very practical, but it serves its purpose. What’s important is this: if you see a function or method with two or more sets of parameters, you can just use it the same way you would a function with just one consolidated list of parameters. No muss, no fuss, no worries. Now, let’s get to the interesting stuff!
Implicit parameters as magical dependency injectors
Given my fancy numerical library in its original form, users can use these three functions to write out numbers…
scala> val formatter = NumberFormat.getPercentInstance()scala> reportError(1.2, formatter)
val res0: String = 120% isn't allowed here.scala> printDouble(0.3, formatter)
val res1: String = 60%
…and things are “okay”, but isn’t it a bit cluttered, always passing the formatter to every function every time? This isn’t the Scala way of doing things. Scala developers love for their code to look neat and clean and concise. We hate boilerplate.
What I really want to do is declare my number format once, up around the top of a code-block or object declaration, and then not to think about it again.
The way to do this is to treat the number format as an implicit parameter to each of the functions, like this:
def makeString(n: Double)(implicit f: NumberFormat): String =
f.format(n)def reportError(n: Double)(implicit f: NumberFormat): String =
makeString(n) + " isn't allowed here."def printDouble(n: Double)(implicit f: NumberFormat): String =
makeString(n * 2)
I can now use the functions just like I had before, but with the two parameter groupings like this:
scala> val formatter = NumberFormat.getPercentInstance()scala> reportError(1.2)(formatter)
val res0: String = 120% isn't allowed here.scala> printDouble(0.3)(formatter)
val res1: String = 60%
Or since I told Scala that the last grouping has implicit parameters, I can declare my formatter instance as implicit once, and the compiler will automatically inject it into the methods.
scala> implicit val formatter = NumberFormat.getPercentInstance()scala> reportError(1.2)
val res0: String = 120% isn't allowed here.scala> printDouble(0.3)
val res1: String = 60%
By using the implicit declaration, the Scala compiler lets me clean things up so that my eyes aren’t distracted by the formatter. It’s just secondary, boilerplate functionality that isn’t as important in terms of the functionality of the code I write.
Now, I still had to declare the formatter in the actual method declarations, and I had to curry the function declarations and add the implicit keyword, so my function declarations don’t look any shorter, but if I’m writing a library, the users of my library get to focus on the important stuff and their code is clean and elegant. (They still have to remember to declare their implicit variable somewhere. You might have run into something like this when using something like Scala’s Future where you were expected to declare an implicit ExecutionContext
. If so, now you know what that was all about.)
Using the alternate “implicitly” syntax
Just to be complete, there’s an alternative way of grabbing implicit values out of the ether instead of writing them as an implicit parameter group in your method definition. Instead of writing something like this:
def makeString(n: Double)(implicit fmt: NumberFormat): String = {
fmt.format(n)
}
We can instead write things this way:
def makeString(n: Double): String = {
val fmt = implicitly[NumberFormat]
fmt.format(n)
}
…or simply
def makeString(n: Double) = implicitly[NumberFormat].format(n)
Internally, the compiler is doing the same thing: it’s taking a promise that when you call the function makeString
there will be an implicit val somewhere in context (in your code or referenced with an import
statement) of the NumberFormat type that it’ll be able to find.
Doing this with Scala 3
Scala 3 is pretty similar, with implicit
replaced by using
in the implicit parameter and implicit val
replaced by given
in the implicit instance declaration.
def makeString(n: Double)(using f: NumberFormat): String =
f.format(n)def reportError(n: Double)(using f: NumberFormat): String =
makeString(n) + " isn't allowed here."def printDouble(n: Double)(using f: NumberFormat): String =
makeString(n * 2)given fmt: NumberFormat = NumberFormat.getPercentInstanceprintDouble(2.32424)
val res0: String = 465%
or rather than the old implicitly
syntax, we can use summon
which “summons” the implicit value out of the ether like this:
def makeString(n: Double): String =
val fmt = summon[NumberFormat]
fmt.format(n)
or simply
def makeString(n: Double) = summon[NumberFormat]format(n)
We will show the use of this in the final section where we demonstrate putting things together with type classes.