Complicit Implicits
Thursday 9 January 2014 at 14:41 GMT
Imagine we have a calculator, written in Scala.
class Calculator {
def run() {
val printer = new Printer
printer.print("The answer is always ")
printer.print(7)
printer.newLine()
}
}
object Calculator extends App {
new Calculator().run()
}
It does one thing and it does it well. (Not the thing it says on the tin, but we'll get there.) It makes use of a Printer
, which is a type of object that knows how to print things.
import java.io.PrintStream
class Printer {
def print(value: Int)(implicit out: PrintStream) = out.print(value)
def print(value: Double)(implicit out: PrintStream) = out.print(value)
def print(value: String)(implicit out: PrintStream) = out.print(value)
def newLine()(implicit out: PrintStream) = out.println()
}
Together, they make music. Or would, if we made one small change:
class Calculator {
implicit val output = System.out
...
}
That's more like it. Compiling, running, doing something useless. It's a good starting point. So let's take it up a notch and fake out some actual calculation:
class Calculator {
private implicit val input = System.in
private implicit val output = System.out
private val scanner = new Scanner
private val printer = new Printer
private val tokenizer = new Tokenizer
private val calculationParser = new CalculationParser
def run() {
val line = scanner.readLine()
val tokens = tokenizer.tokenize(line)
val calculation = calculationParser.formExpression(tokens)
val result = calculation.evaluate()
printer.print(result)
printer.newLine()
}
}
Magnificent, isn't it? Sure, it doesn't actually do anything yet, because all those other classes haven't been implemented, but we now have a structure in place.
My only issue with it is that it doesn't fully follow SOLID principles. It depends upon concrete objects, not abstractions, and it's not open for extension; changes will require modification. We can fix both of these issues by asking for the dependencies, rather than constructing them upon initialisation:
class Calculator(scanner: Scanner, printer: Printer, tokenizer: Tokenizer, calculationParser: CalculationParser) {
...
}
For this to work, we need to adjust the application object to match:
object Calculator extends App {
private implicit val input = System.in
private implicit val output = System.out
private val scanner = new Scanner
private val printer = new Printer
private val tokenizer = new Tokenizer
private val calculationParser = new CalculationParser
new Calculator(scanner, printer, tokenizer, calculationParser).run()
}
Magnificent. Our new version is easy to test as a unit, rather than having to write an integrated test. In addition, because it depends upon abstractions, I can easily change the behaviour without having to change the class itself. For example, if I wanted to use Reverse Polish notation instead, I could just substitute in a different CalculationParser
.
All we need to do now is hit compile and enjoy the magic.
Calculator.scala:32: error: could not find implicit value for parameter in: java.io.InputStream
val line = scanner.readLine()
^
Calculator.scala:36: error: could not find implicit value for parameter out: java.io.PrintStream
printer.print(result)
^
Calculator.scala:37: error: could not find implicit value for parameter out: java.io.PrintStream
printer.newLine()
^
three errors found
What.
It's obvious in hindsight: the Calculator
invokes methods on the scanner and printer that require knowledge of the implicit InputStream
and PrintStream
. We could pass them in to the Calculator
constructor, but that's pretty ugly. The Printer
and Scanner
classes exist to provide a useful abstraction on top of these types; if we also pass in their dependencies, what's the point of them existing in the first place? There must be a better way.
Well, you guessed it. There is. The clue is in the word "dependency"; if the Printer
depends on PrintStream
, can't we make that explicit?
Of course we can:
class Printer(out: PrintStream) {
def print(value: Int) = out.print(value)
def print(value: Double) = out.print(value)
def print(value: String) = out.print(value)
def newLine() = out.println()
}
That was easy, right? And we've managed to reduce duplication, which is lovely.
Some of you may, at this point, be shaking your heads, wondering why I wrote this code in the first place. It might seem alien to you, but this approach is the default taken in most Scala projects, including those we all depend upon. The specific example that provoked this article was the WebBrowser
trait in ScalaTest, in which pretty much all the methods rely on an implicit WebDriver
object being present and in scope. This seems fine initially, but then I wanted to create a page object. Extracting out the code that depended on WebBrowser
was a nightmare; because of the implicit dependency on the WebDriver
object, I had to pass it around everywhere, rather than just constructing it once and encapsulating it.
In Scala, implicit
s are the bane of my existence. Please stop.
Next week, I want to talk about traits and the Cake pattern. Assuming I don't shoot myself first.
If you enjoyed this post, you can subscribe to this blog using Atom.
Maybe you have something to say. You can email me or toot at me. I love feedback. I also love gigantic compliments, so please send those too.
Please feel free to share this on any and all good social networks.
This article is licensed under the Creative Commons Attribution 4.0 International Public License (CC-BY-4.0).