Journey of Implicits in Scala — part 1
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
- Introduction to implicit methods or variable definitions and method parameter lists.
- Type Annotations in Scala
- Type classes in Scala
- Implicit hell and how painfull it really is?
- 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 helperscala> import helper.Foo
import helper.Fooscala> def findAFoo(implicit foo : Foo) = println(foo)
findAFoo: (implicit findAFoo: helper.Foo)Unitscala> 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 helperscala> 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 helperscala> implicitly[helper.B]
res0: helper.B = My B
We create a helper
object. Inside is a trait B
. helper
object 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)Unitscala> 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.Stringscala> 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