13

When looking at the source of some Scala libraries, e.g. shapeless, I often find traits named LowPriorityImplicits.

Can you please explain this pattern? What is the problem that is solved, and how does the pattern solve it?

Daniel Werner
  • 1,350
  • 16
  • 26
ziggystar
  • 28,410
  • 9
  • 72
  • 124

1 Answers1

21

That pattern allows you to have hierarchy of implicits avoiding ambiguity-related errors by the compiler and providing a way to prioritise them. As an example consider the following:

trait MyTypeclass[T] { def foo: String }
object MyTypeclass {
  implicit def anyCanBeMyTC[T]: MyTypeclass[T] = new MyTypeclass[T] { 
    val foo = "any" 
  }

  implicit def specialForString[T](implicit ev: T <:< String): MyTypeclass[T] = new MyTypeclass[T] {
    val foo = "string"
  }
}

println(implicitly[MyTypeclass[Int]].foo) // Prints "any"
println(implicitly[MyTypeclass[Boolean]].foo) // Prints "any"
println(implicitly[MyTypeclass[String]].foo) // Compilation error

The error you get in the last line is:

<console>:25: error: ambiguous implicit values:
  both method anyCanBeMyTC in object MyTypeclass of type [T]=> MyTypeclass[T]
  and method specialForString in object MyTypeclass of type [T](implicit ev: <: <[T,String])MyTypeclass[T]
  match expected type MyTypeclass[String]
       println(implicitly[MyTypeclass[String]].foo)

This wouldn't compile because the implicit resolution will find ambiguity; in this case it is a bit artificial in that we are defining the String case using the implicit evidence in order to trigger the ambiguity when we could just define it as implicit def specialForString: MyTypeclass[String] = ... and not have any ambiguity. But there are cases where you need to depend on other implicit parameters when defining implicit instances and using the low-priority pattern you can write it as follows and have it work fine:

trait MyTypeclass[T] { def foo: String }

trait LowPriorityInstances {
  implicit def anyCanBeMyTC[T]: MyTypeclass[T] = new MyTypeclass[T] { 
    val foo = "any" 
  }
}

object MyTypeclass extends LowPriorityInstances {
  implicit def specialForString[T](implicit ev: T <:< String): MyTypeclass[T] = new MyTypeclass[T] {
    val foo = "string"
  }
}

println(implicitly[MyTypeclass[Int]].foo) // Prints "any"
println(implicitly[MyTypeclass[Boolean]].foo) // Prints "any"
println(implicitly[MyTypeclass[String]].foo) // Prints "string"

It is also worth noting that this pattern is not limited to two layers but you can create a hierarchy of traits and have in them implicit definitions that go from more specific to more generic going up the inheritance tree.

Aldo Stracquadanio
  • 6,167
  • 1
  • 23
  • 34
  • Does this work with typeclasses you don't control or is modifying the companion object required? – gregghz Mar 31 '20 at 21:17
  • I'm not entirely sure if I have to be honest but you can make an experiment by putting the instances in a generic `object` (not a companion), have it extend a low-priority trait and then importing the object explicitly. – Aldo Stracquadanio Apr 02 '20 at 08:02