Field Notes

Scala/Play, Part 11: Scala 2 Implicit Classes - Where Did That Method Come From?

Scala/Play, Part 11: Scala 2 Implicit Classes - Where Did That Method Come From?

I had just written "POL-001".lookup, and Scala found a policy. Coming from Java, my reaction was fairly loud. A string appeared to have gained a business method, and the repository argument had disappeared from the call.

The explanation clicked when I expanded the expression into ordinary code: construct a wrapper, then call its method with a repository. This part of my Scala 2 learning lab follows that expansion first, before using the shortcuts.

Part 10 covered implicit parameters. Here I add implicit classes and connect the two. The examples use Scala 2.13.18 and fictional insurance data; Play and Apache Pekko remain later course topics.

Zakaria's navy-hoodie mascot excitedly examines a transparent exhibit with a wrapper frame and a separate dependency cartridge
The moment the shortcut makes sense: open the casing and inspect the ordinary operations underneath.

Start with a wrapper I already understand

Suppose I want a policy number to produce a display label. An ordinary class can hold the string and provide that behavior:

class PolicyNumberOps(private val number: String) {
  def asPolicyLabel: String = "Policy " + number
}

val policyNumber: String = "POL-001"
val wrapper = new PolicyNumberOps(policyNumber)
val label = wrapper.asPolicyLabel

private val number stores the constructor argument for use inside the wrapper. def declares the method; its final expression supplies the result. The method has no parameter list, so I access it without parentheses. Ops is just a conventional name for operations.

The values are concrete: the original string is "POL-001", the wrapper holds that string, and the method returns "Policy POL-001". The analogous Java call would be new PolicyNumberOps(policyNumber).asPolicyLabel(), given a Java wrapper with that method.

Let Scala supply the wrapping step

Now I place the class inside an object and add implicit:

object PolicyNumberSyntax {
  implicit class PolicyNumberOps(private val number: String) {
    def asPolicyLabel: String = "Policy " + number
  }
}

PolicyNumberSyntax groups the operations. Scala 2 requires implicit classes to be nested inside an object, class, or trait. The implicit class declaration also generates an implicit conversion that constructs the wrapper from its string argument. The official Scala 2 documentation describes this mechanism and its restrictions.

At the call site, I opt in:

import PolicyNumberSyntax._

val policyNumber: String = "POL-001"
val label = policyNumber.asPolicyLabel

The underscore is Scala 2's wildcard import: it brings the object's members into scope, including the generated conversion. String has no asPolicyLabel member. With this conversion available, the compiler can adapt the receiver to PolicyNumberOps and call the wrapper's method.

The explicit equivalent is still available:

new PolicyNumberSyntax.PolicyNumberOps(policyNumber)
  .asPolicyLabel

The method belongs to the wrapper. Java's String class has not changed, and policyNumber still has type String. The import changes which conversion the compiler can find in this scope; it does not globally modify every string.

Three stages: a String without asPolicyLabel, its PolicyNumberOps wrapper, and the resulting Policy POL-001 label
Extension-style syntax uses a method on a wrapper. The original value remains a String.

A method on the wrapper can need a dependency

Formatting a label only needs the number. Finding a policy also needs a repository. Our existing contract already exposes:

def find(number: String): Option[PolicySnapshot]

I could add an ordinary method to the wrapper:

def lookup(repository: PolicyRepository): Option[PolicySnapshot] =
  repository.find(number)

The constructor supplied number; this method call supplies repository. The body uses both. Applying Part 10's syntax only changes the method's parameter list:

def lookup(implicit repository: PolicyRepository): Option[PolicySnapshot] =
  repository.find(number)

This method sits inside PolicyNumberOps, alongside asPolicyLabel. Its implicit parameter allows Scala to supply a suitable repository when I omit the argument. I can still pass it explicitly. The Scala documentation on implicit parameters describes that argument search.

Remove one explicit step at a time

Here is the repository used in the runnable example:

implicit val localStore: PolicyRepository =
  new InMemoryPolicyRepository(
    List(PolicySnapshot("POL-001", 600, 3))
  )

With PolicyNumberSyntax._ imported, these three calls produce the same result:

// Explicit wrapper and explicit repository
new PolicyNumberSyntax.PolicyNumberOps(policyNumber).lookup(localStore)

// Scala supplies the wrapper; I supply the repository
policyNumber.lookup(localStore)

// Scala supplies the wrapper and the repository argument
policyNumber.lookup

There are two separate conveniences. The imported implicit class makes lookup available on this receiver. The implicit value supplies an argument to that method. In this example the wrapper stores the number, while each lookup call receives a repository.

Comparison of three lookup calls showing which ones make the wrapper and repository argument explicit
The fully expanded expression exposes both inputs: the string passed to the wrapper and the repository passed to lookup.

My Spring comparison stops at ordinary dependency passing. The compiler selects the argument at the call site; there is no Spring container involved in this lab. At runtime, the selected repository object's find implementation performs the lookup.

The explicit form keeps control visible

Even with localStore available implicitly, I can choose an empty repository for one call:

val emptyStore: PolicyRepository =
  new InMemoryPolicyRepository(List.empty[PolicySnapshot])

policyNumber.lookup(emptyStore) // None

This is useful when reading unfamiliar code: expand the receiver adaptation, then expose the method argument. A missing policy produces None after a successful call. A missing implicit dependency is a compilation problem, as explored in Part 10.

I would use syntax like this selectively. A short expression saves repetition, but readers still need to discover the operations and dependencies behind it. Giving syntax a clearly named object and keeping explicit calls available makes that investigation easier.

Run the evidence

The complete lesson at commit 7b43025 contains the wrapper and assertions. From the lab root:

sbt "runMain learning.PremiumLesson"

The run passed on Scala 2.13.18 with sbt 1.12.15. All three lookup forms returned Some(PolicySnapshot(POL-001,600,3)); an unknown number and the explicitly chosen empty repository returned None. The earlier lesson assertions passed too.

I still enjoy how surprising "POL-001".lookup looks from Java. Now I can expand it into code I recognize, and that makes the surprise useful.