Implicit inference in Scala

Many beginner and not so Scala developers take implicits as a moderately useful feature. Usage is usually limited to passing ExecutionContext



 to Future



. Others avoid the implicit and view the opportunity as harmful.





Code like this scares a lot of people:





implicit def function(implicit argument: A): B
      
      



But I think this mechanism is an important advantage of the language, let's see why.





Briefly about implicits

In general, implicits is a mechanism for automatic code completion during compilation:





  • for implicit arguments, the value is automatically substituted





  • for implicit conversions, the value is automatically wrapped in a method call





I will not go deep into this topic, who are interested in watching this video from the creator of the language . In a nutshell, this one mechanism is used in several different cases (and in Scala 3 will be split into several separate features):





  1. Passing implicit context (like ExecutionContext



    )





  2. Implicit conversions (deprecated)





  3. Extension methods (syntactic sugar for adding methods to existing types)





  4. Type classes and implicit resolution





Type classes and implicit inference

By themselves, type classes do not bring any novelty or revolution - they are simply the implementation of methods "for" the type, and not "in" the type itself, it's like the difference between Comparable



 & Ordering



( Comparator



 in Java):





Comparable



  :





class Person(age: Int) extends Comparable[Person] {
  override def compareTo(o: Person): Int = age compareTo o.age
}
def max[T <: Comparable[T]](xs: Iterable[T]): T = xs.reduce[T] {
  case (a, b) if (a compareTo b) < 0 => b
  case (a, _) => a
}
      
      



Ordering



  , :





case class Person(age: Int)

implicit object ByAgeOrdering extends Ordering[Person] {
  override def compare(o1: Person, o2: Person): Int = o1.age compareTo o2.age
}
def max[T: Ordering](xs: Iterable[T]): T = xs.reduce[T] {
  case (a, b) if Ordering[T].lt(a, b) => b
  case (a, _) => a
}
// is syntactic sugar for
def max[T](xs: Iterable[T])(implicit evidence: Ordering[T]): T = ...
      
      



.





. :





implicit val value: A = ???
implicit def definition: B = ???
implicit def conversion(argument: C): D = ???
implicit def function(implicit argument: E): F = ???
      
      



? conversion



, , : value



, definition



 & function



  . : val



 , def



  . – .





– , , .





– , – , :





implicit def pairOrder[A: Ordering, B: Ordering]: Ordering[(A, B)] = {
  case ((a1, b1), (a2, b2)) if Ordering[A].equiv(a1, a2) => Ordering[B].compare(b1, b2)
  case ((a1,  _), (a2,  _)) => Ordering[A].compare(a1, a2)
}
// again, just syntactic sugar for:
implicit def pairOrder[A, B](implicit a: Ordering[A], b: Ordering[B]): Ordering[(A, B)] = ...
      
      



, :





val values = Seq(
  (Person(30), ("A", "A")),
  (Person(30), ("A", "B")),
  (Person(20), ("A", "C"))
)
max(values) // => (Person(30),(A,B))
      
      



Seq[(Person, (String, String))]



  Ordering



  :





max(values)(
  pairOrder(
    ByAgeOrdering, 
    pairOrder(Ordering.String, Ordering.String)
  )
)
      
      



So implicit inference allows you to describe general inference rules and instruct the compiler to bring these rules together and get a specific implementation of the type class. Adding your own type or your own rules does not need to describe everything from the beginning - the compiler will combine everything itself to obtain the desired object.





And most importantly, if the compiler fails, you will receive a compilation error, not a runtime error, and you will be able to fix the problem right away. Although, of course, there is a fly in the ointment in the ointment - if the compiler failed, you do not know which link in the chain was missing - it is not always easy to debug this.





Hopefully the implicit is now a little more explicit.








All Articles