Scala3 Implicit
Scala3 did lots of improvement in Implicit, which is the most painful feature in Scala2.
A quick summary is
- Improve the error log to help the developer find the correct implicit instance.
- Split the implicit feature into given/using and extension to make the feature more clear.
Now let’s have a taste of these features.
How to pass parameters implicitly?
Scala2
def add(x: Int)(implicit y: Int): Int = x + y
implicit val instance: Int = 10 // :Int = 10
add(1) // :Int = 11
add(1)(5) // :Int = 6
Scala3
def add(x: Int)(using y: Int): Int = x + y
given instance: Int = 10
add(1) // :Int = 11
add(1)(using 5) // :Int = 6
Difference
- Replace
implicit
withusing
in function parameter - Replace
implicit val
withgiven
in instance definition - Use
using
explicitly when giving explicit value
With these changes, the code readability is improved.
At least we can search given
to find the implicit instances and search using
to find the implicit parameters.
How to use context bound?
Scala2
trait Show[A] {
def show(x: A): String
}
def printListWithUsing[A](list: List[A])(implicit show: Show[A]): String = list.map(x => show.show(x)).mkString(";")
def printListWithContextBound[A:Show](list: List[A]): String = list.map(x => implicitly[Show[A]].show(x)).mkString(";")
implicit val intShow: Show[Int] = new Show[Int] {
def show(x: Int): String = x.toString
}
printListWithUsing(List(1, 2, 3, 4)) // :String = "1;2;3;4"
printListWithContextBound(List(1, 2, 3, 4)) // :String = "1;2;3;4"
Scala3
trait Show[A] {
def show(x: A): String
}
def printListWithUsing[A](list: List[A])(using show: Show[A]): String = list.map(x => show.show(x)).mkString(";")
def printListWithContextBound[A:Show](list: List[A]): String = list.map(x => summon[Show[A]].show(x)).mkString(";")
given intShow: Show[Int] with {
def show(x: Int): String = x.toString
}
printListWithUsing(List(1, 2, 3, 4)) // :String = "1;2;3;4"
printListWithContextBound(List(1, 2, 3, 4)) // :String = "1;2;3;4"
Difference
- Replace
implicitly
withsummon
-
Replace
new class
withwith
Not like Scala2, we don’t need to new the implicit class explicitly, all the members of class will be wrapped by
with
keyword.implicit val intShow: Show[Int] = new Show[Int] { ... } // Scala2 given intShow: Show[Int] with { ... } // Scala3
How to add function to an existing class?
Scala2
trait Show[A] {
def show(x: A): String
}
implicit class ShowOps[A: Show](v: A) {
def show(): String = implicitly[Show[A]].show(v)
}
implicit val intShow: Show[Int] = new Show[Int] {
def show(x: Int): String = x.toString
}
implicit def listShow[A: Show]: Show[List[A]] = new Show[List[A]] {
def show(v: List[A]): String = v.map(_.show()).mkString(";")
}
1.show() // :String = "1"
List(1, 2, 3).show() // :String = "1;2;3"
Scala3
trait Show[A] {
extension (v: A) {
def show(): String
}
}
given intShow: Show[Int] with {
extension (v: Int) {
def show(): String = v.toString()
}
}
given listShow[A: Show]: Show[List[A]] with {
extension (v: List[A]) {
def show(): String = v.map(_.show()).mkString(";")
}
}
1.show() // :String = 1
List(1, 2, 3).show() // :String = "1;2;3"
Difference
In Scala2, we usually define a type class first, then use an implicit class to add the function to the existing type.
In Scala3, it becomes straightforward, use a new keyword extension
to do that.
The magic thing is all the methods in extension
will be translated to functions, the following two expressions are equal
extension(v: Int) {
def demo():String = v.toString
}
// equals to def demo(v: Int)() = v.toString
1.demo() // :String = 1
demo(1)() // :String = 1
Summary
According to my experience, Scala3 implicit is easier than Scala2, but there are still complicated rules for implicit finding, will talk about them later.
You can find the demo code here
Comments