Puzzler 29

Implicit Kryptonite

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()

Possibilities

  1. The first, second, and third statements print:
      Found a knife
      ALARM! ALARM!
      Found a detonator
    

    and the fourth fails to compile.

  2. Prints:
      Found a knife
      ALARM! ALARM!
      Found a detonator
      Test successful. Well done!
    
  3. The first and second statements print:
      Found a knife
      ALARM! ALARM!
    

    and the third and fourth fail to compile.

  4. Prints:
      Found a knife
      ALARM! ALARM!
      Found a shoe
      Test successful. Well done!
    

Explanation

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.

Discussion

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.

image images/moralgraphic117px.png
  1. Names of implicits matter! Importing an implicit value of the same name and type as an existing implicit will remove the existing implicit from the set of applicable options considered by the compiler. Importing an implicit of the same type, but with a different name, can lead to "ambiguous implicit values" compiler errors.
  2. When defining a set of implicits that are intended to be overridable, declare them in a "default context" class or trait that can be extended. Define overriding implicits in a subclass or subtrait of the default context. In this case, higher priority implicits do not need to have the same name as the implicits they are intended to replace.

Footnotes for Chapter 29:

[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]

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.27.74