Journey of Implicits in Scala — part 1

ayush mittal
6 min readMar 2, 2022
Photo by Daniele Levis Pelusi on Unsplash

Implicits are the most ambivalent feature in Scala. Highly popular and widely used in many frameworks and libraries. And on the same hand controversial for being dangerous and making things work magically which becomes a challenge for those who are new to the language. In this 5 part series we will look at each aspect. We will shall go as follows

  1. Introduction to implicit methods or variable definitions and method parameter lists.
  2. Type Annotations in Scala
  3. Type classes in Scala
  4. Implicit hell and how painfull it really is?
  5. Scala 3’s approach to Type Classes.

So first things first , what are implicits?

Introduction to Implicits

Scala provides an implicit keyword to be used on methods or variable and on method parameter lists. When this keyword is seen on a method or variable it is basically a signal to compiler to use this methods or variables during an implicit resolution. Implicit resolution is when the compiler determines that a piece of information is missing in code, and it must be looked up. The implicit keyword can also be used at the beginning of a method parameter list. This tells the compiler that the parameter list might be missing, in which case the compiler should resolve the parameters via implicit resolution.

Lets look at implicit resolution in work :

scala> def addNumber(x: Int)(implicit y: Int) = x + y 

The addNumber method declares a non-implicit parameter named x of type int and a single implicit parameter y of type Int. This function will add these two int numbers. The parameter y is marked with implicit, which means that we don’t need to use it. If it’s left off, the compiler will look for a variable of type Int in the implicit scope. Let’s look at the following example:

scala> addNumber(2) 
error: could not find implicit value for parameter y: Int

The addNumber method is called without specifying any argument for the second parameter. The compiler complains that it can’t find an implicit value for the y parameter. We’ll provide one, as follows:

scala> implicit val couldBeY = 2
couldBeY: Int = 2

The couldBeY value is defined with the implicit keyword. This marks it as available for implicit resolution. Since this is in the REPL, the value will be available in the implicit scope for the rest of the REPL session.

scala> addNumber(2) 
res2: Int = 4

The call to addNumber succeeds and returns the sum. The compiler was able to successfully complete the function call. We can still provide the parameter if desired

scala> addNumber(2)(3) 
res3: Int = 5

This method call passes the second parameter y with a value of 2. Because the method call is complete, the compiler doesn’t need to look up a value using implicits.

Implicit Resolution

Following are the rules to look up entities marked as implicit

  • The implicit entity binding is available at the lookup site with no prefix — that is, not as foo.x but only x.
  • If there are no available entities from this rule, then all implicit members on objects belong to the implicit scope of an implicit parameter’s type.

The first rule is that the Scala compiler will search for an identifier in the local scope with no prefix.

scala> class Fooscala> def findAFoo(implicit x : Foo) = x
findAFoo: (implicit x: Foo)Foo
scala> implicit val test = new Foo
test: Foo = Foo$$anon$1@223dee

The findAFoo method is declared with an implicit parameter list of a single Foo. The next line defines a val test with the implicit marker. This makes the identifier, test, available on the local scope with no prefix. If we were to write test in the REPL, it would return a value of type Foo. When we write this method call, findAFoo, the compiler will rewrite it as findAFoo(test)

The second rule for implicit lookup is used when the compiler can’t find any available implicits using the first rule. In this case, the compiler will look for implicits defined within any object in the implicit scope of the type it’s looking for. The implicit scope of a type is defined as all companion modules that are associated with that type. This means that if the compiler is looking for a parameter to the method def findAFoo(implicit x : Foo), that parameter will need to conform to the type Foo. If no value of type Foo is found using the first rule, then the compiler will use the implicit scope of Foo. The implicit scope of Foo would consist of the companion object to Foo.

scala> object helper {
| trait Foo
| object Foo {
| implicit val x = new Foo {
| override def toString = "Companion Foo"
| }
| }
| }
defined module helper
scala> import helper.Foo
import helper.Foo
scala> def findAFoo(implicit foo : Foo) = println(foo)
findAFoo: (implicit findAFoo: helper.Foo)Unit
scala> findAFoo
Companion Foo

The helper object is used so we can define a trait and companion object within the REPL. Inside, we define a trait Foo and companion object Foo. The companion object Foo defines a member x of type Foo that’s available for implicit resolution. Next we import the Foo type from the helper object into the current scope. Next is the definition of method. The method takes an implicit parameter of type Foo. When called with no argument lists, the compiler will use the implicit val x defined on the companion.

Because the implicit scope is looked at second, we can use the implicit scope to store default implicits while allowing users to import their own overrides as necessary.

Implicit Scope with Type Parameters

Scala allows us to define the implicit scope of a type to include the companion objects of all types or subtypes included in the type’s parameters. This means, for example, that we can provide an implicit value for List[Foo] by including it in the type Foo’s companion object. Here’s an example:

scala> object helper {
| trait Foo
| object Foo {
| implicit val list = List(new Foo{})
| }
| }
defined module helper
scala> def findAFooList(implicit fooList : List[Foo]) = fooList
findAFooList: (implicit fooList: List[helper.Foo])List[helper.Foo]
scala> findAFooList
res0: List[helper.Foo] = List(helper$Foo$$anon$1@2e31h1h)

The helper object is used, again, to create companion objects in the REPL. The helper object contains a trait Foo and its companion object. The companion object contains an implicit definition of a List[Foo] type. The next line defines a method that has a List[Foo] parameter passed implicitly. The method simply returns the passed parameter. When called with no argument lists, the compiler will use the implicit val list defined on the companion to complete the method call.

Implicit Scope with Nesting

Implicit scope resolution also checks the companion objects from outer scopes if a type is defined in an inner scope. Here is an example.

scala> object helper {
| trait B
| implicit def aPossibleB = new B {
| override def toString = "My B"
| }
| }
defined module helper
scala> implicitly[helper.B]
res0: helper.B = My B

We create a helperobject. Inside is a trait B . helperobject also defines an implicit method that creates an instance of trait B. Next line is call to Scala’s implicitly function. We can use this function to look up a type using the current implicit scope. The implicitly function is defined as def implicitly[T](implicit arg : T) = arg. It uses the type parameter T to allow us to reuse it for every type we’re looking for.

Advantages of Implicits

Implicits can be used to enhance existing classes. We would like to call expression on existing classes without changing the existing class. Instead we would provide a conversion method that can allows existing types to be converted into types into which the expression can be called. Let’s take an example

scala> def showMe(msg : String) = println(msg)
foo: (showMe: String)Unit
scala> showMe(47)
<console>:9: error: type mismatch;
found : Int(5)
required: String
showMe(5)

The showMe method is defined to take a String and print it. The call to showMe using the value 47 fails, as there’s a type mismatch. An implicit conversion can make this succeed. Here is one:

scala> implicit def intToString(x : Int) = x.toString
intToString: (x: Int)java.lang.String
scala> showMe(47)
47

This usage of implicits allows adding methods of one library to types present in another library. A good example is the scala.collection.JavaConversions package. It allows conversion between Scala collection and java collection types. The package defines a set of implicit conversions that can be imported into the current scope to allow automatic conversion between Java collections and Scala collections and to “add” methods to the Java collections.

Hopefully you have a good idea of what implicits are what advantages they bring. Next we will look at how they can be used with types to achieve what is known as Ad-hoc polymorphism.

Further readings

--

--