Scala's implicits provide a flexible mechanism to access context-specific values and behavior. By changing the implicits in scope, you can switch contexts in different parts of your application easily.
This code example models a simple baggage scanner that operates in two modes, or contexts: normal operation and a special "test mode." In normal operation, the scanner's console indicates which type of item is being scanned and the alarm button is "live," triggering the alarm when activated.
To ensure that the operator keeps paying attention, the scanner also has a test mode that is activated at random intervals. In test mode, the console ignores the actual item inside the scanner and pretends to have found a dangerous item. If the operator hits the alarm button, as they should, the scanner congratulates them for completing the test successfully.
The behavior of the scanner is defined by two implicits, a console and a handler, for the two contexts, normal operation and test mode. The scanner is switched from normal to test mode as items pass through it.
What is the result of executing the following code in the REPL?
object Scanner { trait Console { def display(item: String) } trait AlarmHandler extends (() => Unit)
def scanItem(item: String)(implicit c: Console) { c.display(item) } def hitAlarmButton()(implicit ah: AlarmHandler) { ah() } }
object NormalMode { implicit val ConsoleRenderer = new Scanner.Console { def display(item: String) { println(s"Found a ${item}") } } implicit val DefaultAlarmHandler = new Scanner.AlarmHandler { def apply() { println("ALARM! ALARM!") } } } object TestMode { implicit val ConsoleRenderer = new Scanner.Console { def display(item: String) { println("Found a detonator") } } implicit val TestAlarmHandler = new Scanner.AlarmHandler { def apply() { println("Test successful. Well done!") } } }
import NormalMode._ Scanner scanItem "knife" Scanner.hitAlarmButton()
import TestMode._ Scanner scanItem "shoe" Scanner.hitAlarmButton()
Found a knife ALARM! ALARM! Found a detonator
and the fourth fails to compile.
Found a knife ALARM! ALARM! Found a detonator Test successful. Well done!
Found a knife ALARM! ALARM!
and the third and fourth fail to compile.
Found a knife ALARM! ALARM! Found a shoe Test successful. Well done!
You may suspect that importing the test mode implicits will cause the third and fourth statements to fail to compile due to ambiguous implicit values in scope. Otherwise, all four statements should work, or not? Surely the names of the implicits have no bearing on the outcome?
Actually, they do—the correct answer is number 1:
scala> Scanner scanItem "knife" Found a knife
scala> Scanner.hitAlarmButton() ALARM! ALARM! ... scala> Scanner scanItem "shoe" Found a detonator
scala> Scanner.hitAlarmButton() <console>:17: error: ambiguous implicit values: both value DefaultAlarmHandler in object NormalMode of type => Scanner.AlarmHandler and value TestAlarmHandler in object TestMode of type => Scanner.AlarmHandler match expected type Scanner.AlarmHandler Scanner.hitAlarmButton() ^
How can this be? When the operator first hits the alarm button, the compiler is able to choose between two implicits of the same type, but not the second time around? Could this be related to the names of the implicit values? If so, isn't it the type of an implicit that matters, rather than the name?
Well, the type of an implicit certainly determines whether it is applicable at a particular point in the code. To understand the observed behavior, you need to look at how the compiler identifies and handles multiple applicable alternatives. And that is where the name of an implicit can come into play.
While you may intuitively expect implicit values to be "special" in some way, the compiler treats them like any other val or def. Therefore, when you import the test mode implicits, the TestMode.ConsoleRenderer shadows the previously imported NormalMode.ConsoleRenderer. When the compiler searches for an implicit Console for the second scanItem call, only one applicable implicit value is actually in scope, so the call compiles.
The two AlarmHandlers, however, have different names. After importing the test mode implicits, two applicable alternatives are therefore in scope, NormalMode.DefaultAlarmHandler and TestMode.TestAlarmHandler. The compiler then applies the standard static overloading resolution algorithm[1] to determine a most specific implicit to use—there are no "special" rules for implicits, in other words.[2] Here, neither of the two alternatives is more specific than the other, leading to the compiler error you observe.
Obviously, you can avoid the problem by ensuring that your test mode alarm handler has the same name as the default one you intend to replace:
object TestMode2 { implicit val ConsoleRenderer = new Scanner.Console { def display(item: String) { println("Found a detonator") } } // same name as the alarm handler in NormalMode implicit val DefaultAlarmHandler = new Scanner.AlarmHandler { def apply() { println("Test successful. Well done!") } } }
...
import TestMode2._
scala> Scanner scanItem "shoe" Found a detonator
scala> Scanner.hitAlarmButton() Test successful. Well done!
If you don't know the name of the implicit you are trying to "override" (e.g., because you are importing it from a library), you can extract it from the "ambiguous implicit values" error message. Still, having to identify and keep track of the names of implicits you wish to override is not a particularly satisfactory solution.
Happily, Scala provides a way to override implicits without having to know their names. The trick is ensuring that static overload resolution regards the overriding implicits as more specific than the ones being replaced.
The standard way to do this is to define the default context in a base class or trait from which the "overriding context" inherits, as exemplified by scala.LowPriorityImplicits. Here, this would look something like:
...
class OperatingMode { implicit val ConsoleRenderer = new Scanner.Console { def display(item: String) { println(s"Found a ${item}") } } implicit val DefaultAlarmHandler = new Scanner.AlarmHandler { def apply() { println("ALARM! ALARM!") } } } object NormalMode extends OperatingMode
object TestMode extends OperatingMode { override implicit val ConsoleRenderer = new Scanner.Console { def display(item: String) { println("Found a detonator") } } implicit val TestAlarmHandler = new Scanner.AlarmHandler { def apply() { println("Test successful. Well done!") } } }
import NormalMode._
scala> Scanner scanItem "knife" Found a knife
scala> Scanner.hitAlarmButton() ALARM! ALARM!
import TestMode._
scala> Scanner scanItem "shoe" Found a detonator
scala> Scanner.hitAlarmButton() Test successful. Well done!
In this version of the code example, when the compiler tries to determine the most specific applicable implicit for the second hitAlarmButton call, the test mode handler is more specific. To paraphrase the language specification, "TestAlarmHandler is defined in an object, TestMode, that extends the class, OperatingMode, defining DefaultAlarmHandler."[3]
Bear in mind, however, that for this approach to work the default context must explicitly be written to be extended. If the default implicits are being imported from code that you do not control, such as an external library, you have to hope that the code's authors have adhered to this pattern.
|
[1] Odersky, The Scala Language Specification, Section 6.26.3. [Ode14]
[2] Odersky, The Scala Language Specification, Section 7.2. [Ode14]
[3] Odersky, The Scala Language Specification, Section 6.26.3. [Ode14]