Implicits

6 minute read

In this blog, we will talk about the basic knowledge of Implicits in Scala and some common programming situations where you can use Implicits.

What is Implicits in Scala?

Implicits are a powerful, code-condensing feature of Scala.1

It is usually used in type conversion and function/class parameter and happens in compilation phase.

The workflow of Implicits looks like this

There are six places can define implicits:

  1. Variable

    implicit val a:Int = 1
    
  2. Function

    implicit def int2Str(v:Int):String = v.toString
    
  3. Class

    implicit class Str2Int(v:Int){
        def toInt(v:Int) = v.toInt 
    }
    
  4. Function Parameter

    def sort[A](l:List[A])(implicit order:Ordering[A])
    
  5. Class Parameter

    class Ages(ages:List[Int])(implicit order:Ordering[Int])
    
  6. Type Parameter

    def sort[A:Ordering](l:List[A])
    

There are two scenarios compiler will use implicits

  1. Type doesn’t check

    val a:String = 1
    
  2. Missing parameters

    def sort[A](list:List[A])(implicit order:Order[A])
    
    $ sort(List(1,2,3))
    

There are four rules for implicits1:

  1. Making rule: Only definitions marked implicit are available.

    This is a basic rule of implicits, we can’t imagine compiler will pick up a random function to do the implicits work.

    This rule build a pool of implicits for compiler, compiler can only pick up the proper implicit in this pool to replace the code.

  2. Scope rule: An inserted implicit conversion must be in scope as a single identifier, or be associated with the source or target type of the conversion.

    When I read this sentence, my first question is What is single identifier?, let’s try to anwser it.

    A single identifier is an import expression, we can identify one and only one implict instance

    object IntConverter{
        implicit def int2Str(v:Int):String = v.toString
    }
    
    import IntConverter // this is not a single identifier, compiler won't use   
                        //IntConverter.int2Str to do implicit work
    import IntConverter.int2Str // this is a single identifier
    

    Why do we need the scope rule? Let’s see the following example

    object IntConverter{
        implicit def int2Str(v:Int):String = v.toString
    }
    object IntConverter2{
        implicit def int2Str(v:Int):String = s"Hello ${v}"
    }
    

    Which method should compiler use? right, compiler won’t know, we should tell it. that’s why we need the scope rule, we need to tell compiler exactly which implicit should be used in this scope.

    This rule also involve complexity in our code, we not only care about the code logic but also care about what has been imported. that’s cost we need to pay for the omit functions.

    Let’s talk about scope more, in consideration of common programming situations, Scala add one exception for this rule. There is a fallback scope for implicits, that’s the companion object of source or target type.

    Using this exception, we don’t need to import the implicit as a single identifier. Try the following example

    case class Name(v:String)
    object Name{
        implicit def str2Name(v:String):Name = Name(v)
        implicit def name2Str(n:Name):String = n.v
        implicit val nv:Name = Name("test")
    }
    object Test{
        def f(implicit n:Name)=println(n)
    }
    
    $ val a:Name = "Job"
    a: Name = Name("Job")
    $ val b:String = Name("Job")
    b: String = "Job"
    $ Test.f
    Name(1)
    
  3. One-at-a-time rule: Only one implicit is inserted.

    Compiler only try to fix our code once and this fix is clear and exact, we can’t imagine compiler will compose our code automatically, then the running code is more different with the code writing by us. Let’s try the following example

    case class Name(v:String)
    object Name{
        implicit def str2Name(v:String):Name = Name(v)
    }
    object Test{
        implicit def int2Str(v:Int):String = v.toString
    }
    
    $ import Test.int2Str
    $ val a:String = 1
    a: String = "1"
    $ val a:Name = "Job"
    a: Name = Name("Job")
    $ val a:Name = 1
    cmd26.sc:1: type mismatch;
     found   : Int(1)
     required: ammonite.$sess.cmd22.Name
    val a:Name = 1
                 ^
    Compilation Failed
    

    We can see compiler won’t help us to do the chained conversion Int -> String -> Name, it can just do Int->String or String->Name.

  4. Explicits-first rule: Whenever code type checks as it is written, no implicits are attempted.

    This rule is simple, if our code has no error, compiler won’t use implicits to fix our code. Let’s try the following example

    case class Name(v:String)
    object Name{
        implicit def str2Name(v:String):Name = Name(v)
    }
    
    $ val a:Any = "Job"    # won't use implicits
    a: Any = "Job"
    $ val a:String = "Job" # won't use implicits
    a: String = "Job"
    $ val a:Name = "Job"   # using implicits and replace code with str2Name("Job")
    a: Name = Name("Job")
    

Now we have a rough picture of implicits, Let’s see how to use it in differents programming situations.

How to convert type implicitly?

This is the basic usage of implict, compiler can fix the type error automatically. Let’s see the following example

case class Name(v:String)
object Name{
    implicit def str2Name(v:String):Name = Name(v)
}
$ val a:Name = "Job"   # using implicits and replace code with str2Name("Job")
a: Name = Name("Job")

In this scenario, we usually define an implicit function to do the type conversion, then import this function into the scope, compiler can use it properly.

How to add new function to an existing class?

This is most common usage of implicits, it give us the power to extend the existing library without changing the source code and inheriting the existing class. Let’s see the following example

case class Age(age:Int)

We want to add addition to class Age, then we can use it like this

$ val newAge:Age = Age(10) + 1

We can implement it using implicits like this

object Age {
    implicit class AgeOps(age:Age){
        def + (n:Int):Age = Age(age.age+n)
    }
}
$ import Age._
$ val newAge:Age = Age(10) + 1
newAge: Age = Age(11)

We need to import AgeOps here, because this scenario is not a direct type conversion, compiler can’t find the proper implicits in the companion object.

The behavior of compiler looks like this

  • There is no addition in class Age

  • Is there implicits(variable,function returns or class) has addition?

    Finding class AgeOps

  • Can Age use that implicits?

    Age can be converted to AgeOps in one step

  • Replace Age(10) + 1 with new AgeOps(Age(10)) + 1

How to give the default value of parameters?

Sometimes the parameter of function or class have a common value, but we still want to give user the ability to change this value, then we can use implicit parameter. Let’s see the following example

def sort[A](l:List[A])(implicit order:Ordering[A]) = l.sorted
$ sort(List(1,2,3))                                    # most of time, we will use the 
res37: List[Int] = List(1, 2, 3)                       # ascending order
$ sort(List(1,2,3))(implicitly[Ordering[Int]].reverse) # when we want to use descending 
res38: List[Int] = List(3, 2, 1)                       # order, we can give the order 
                                                       # explicitly

Here we have a default implicit Ordering which is ascending, but user is free to build their own Ordering to override the default one.

We should notice here, if there is no type parameter in parameter, we don’t need implicits, we can use the default parameter directly

def add(a:Int)(b:Int = 1):Int = a+b
$ add(1)()
res40: Int = 2

So in this scenario, you can get more benefit when the parameter depend on type parameter and you should give a default implementation.

How to restrict the type parameter?

For higher kind programming, somethimes we want to add restriction to the type parameter, the common restriction is instance proof: We can create this instance only when the type parameter already has another instances. Let’s see the following example

def sort[A:Ordering](l:List[A]) = l.sorted

A:Ordering means if type A want to use sort, there should be an instance of Ordering[A] in this scope

$ sort(List(1,2,3))
res37: List[Int] = List(1, 2, 3)
$ sort(List(Age(1),Age(2),Age(3)))
cmd41.sc:1: No implicit Ordering defined for ammonite.$sess.cmd31.Age.
val res41 = sort(List(Age(3),Age(2),Age(1)))
                ^
Compilation Failed

You may notice this example is same as the previous example, actually it’s just a sugar syntax of pervious example.

Usually we don’t need to use the implicit parameter in function or class, it’s a just proof of valid type parameter.

Reference

  1. Programming in Scala, Thrid Edition, Martin Odersky.  2

Comments