2. Type classes
Type classes are “Type system constructs that support ad hoc polymorphism”.
In simpler terms:
● Defining behaviour for a group of types, without modifying those types, no inheritance.
● Custom implementation for each type.
● Generate instances that uses those implementations.
More specifically:
● A trait that defines the operations.
● Implicit instances that provide implementations for specific types.
● Methods that use the declared implicit instances.
3. Type classes in Scala 2
trait Encoder[E] {
def encode(value: E): String
}
implicit val booleanEncoder: Encoder[Boolean] = new Encoder[Boolean] {
def encode(value: Boolean): String = if (value) "yes" else "no"
}
def display[E](value: E)(implicit encoder: Encoder[E]): Unit =
println(encoder.encoder(value))
4. Type classes in Scala 3
trait Encoder[E]:
def encode(value: E): String
given Encoder[Boolean] with
def encode(value: Boolean): String = if (value) "yes" else "no"
def display[E](value: E)(using encoder: Encoder[E]): Unit =
println(encoder.encode(value))
val amIHuman = true
display(amIHuman) // yes
5. Defining more encoders with given/using clauses
given Encoder[Boolean] with
def encode(element: Boolean): String = if (element) "yes" else "no"
given Encoder[String] with
def encode(element: String): String = element
given Encoder[Int] with
def encode(element: Int): String = element.toString
6. Deriving given/using clauses
given encodeList[E](using encoder: Encoder[E]): Encoder[List[E]] with
def encode(element: List[E]): String =
element.map(encoder.encode).mkString("[", ", ", "]")
given encodeOption[E](using encoder: Encoder[E]): Encoder[Option[E]] with
def encode(element: Option[E]): String =
element match { case Some(e) => encoder.encode(e) case None => "None" }
7. Defining even more encoders… ?
case class Car(brand: String, year: Int, isNew: Boolean)
given encodePerson(using s: Encoder[String], i: Encoder[Int], b: Encoder[Boolean]
): Encoder[Car] with
def encode(element: Car): String = {
element match
case Car(brand, year, isNew) => {
List("brand: " + s.encode(brand), "year: " + i.encode(year),
"isNew: " + b.encode(isNew)
).mkString("{", ", ", "}")
}
}
display(Car("Toyota", 1997, false)) // {brand: Toyota, year: 1997, isNew: no}
8. Can we save ourselves from this boilerplate nightmare?
Yes
9. Type class derivation
● Automatically generate given instances for type classes which satisfy some simple
conditions.
● Keyword in Scala: derives.
● Implementation using derived method.
case class Car(brand: String, year: Int, isNew: Boolean) derives Encoder
inline def derived[E](using m: Mirror.Of[E]): Encoder[E] = ???
10. Goal for this talk
case class Car(brand: String, year: Int, isNew: Boolean) derives Encoder
inline def derived[E](using m: Mirror.Of[E]): Encoder[E] = ???
● We want to able to derive any case classes and enums automatically deriving Encoder!
● But first… let’s refresh/introduce some concepts before that…
11. Summoning instances
● summon Keyword used to requests given values.
● Similar to implicitly.
● summon can return a more precise type than the type it was asked for, compared to
implicitly.
13. Inlines
● Marking a method definition as inline will, at compile time, copy the right hand side of a
method at the place where is used, instead of invoking it.
● One can have inline arguments, expanding the variable without computing it first.
● You can have conditional inlines and match inlines
● Usages:
○ Performance optimizations.
○ Information that inlines surfaces at compile time.
14. Inlines example
inline def inlineSquare(x: Int): Int = x * x
def normalSquare(x: Int): Int = x * x
val a = InlinesDemo.inlineSquare(5)
val b = InlinesDemo.normalSquare(5)
val a: Int = 25:Int
val b: Int = com.dixa.meetup.InlinesDemo.normalSquare(5)
15. Inlines example
inline def inlinedLog(inline level: String, message:
String): Unit =
inline if level == "debug" then
println(s"[DEBUG] $message")
else if level == "info" then
println(s"[INFO] $message")
else println(s"[UNKNOWN] $message")
def regularLog(level: String, message: String): Unit =
if level == "debug" then
println(s"[DEBUG] $message")
else if level == "info" then
println(s"[INFO] $message")
else println(s"[UNKNOWN] $message")
{
println(
_root_.scala.StringContext.apply(["[DEBUG] ","" :
String]*).s(
["Something happened!" : Any]*)
)
}:Unit
com.dixa.meetup.InlinesDemo.regularLog("debug",
"Something happened!")
16. Compile-time operations (scala.compiletime)
Helper definitions that provide support for compile-time operations over values
● summonInline
○ Delays summoning to the call site of the function, when the type will be concrete to
the compiler so it can know whether is possible to summon or not.
○ Used in conjunction with inline methods.
17. summonInline example
def display[E](value: E)(using encoder: Encoder[E]) = {
println(encoder.encode(value)) // It works!
}
def displaySummonDoesNotCompile[E](value: E) = {
println(summon[Encoder[E]].encode(value))
// No given instance of type com.dixa.meetup.Encoder[E] was found...
}
inline def displaySummonInline[E](value: E) ={
println(summonInline[Encoder[E]].encode(value)) // It works!
}
18. Compile-time operations (scala.compiletime)
Helper definitions that provide support for compile-time operations over values
● summonInline
○ Delays summoning to the call site of the function, when the type will be concrete to
the compiler so it can know whether is possible to summon or not.
○ Used in conjunction with inline methods.
● constValue
○ Produces the constant value represented by a type.
○ Needs to be constant!
19. constValue example
inline def printSize[N <: Int]: Unit = println(s"Size is ${constValue[N]}")
printSize[5] // 5
printSize[Int] // Int is not a constant type; cannot take constValue
20. Compile-time operations (scala.compiletime)
Helper definitions that provide support for compile-time operations over values
● summonInline
○ Delays summoning to the call site of the function, when the type will be concrete to
the compiler so it can know whether is possible to summon or not.
○ Used in conjunction with inline methods.
● constValue
○ Produces the constant value represented by a type.
○ Needs to be constant!
● erasedValue
○ Type erasure is a process where the compiler removes type information during
compilation. In the JVM, generic type parameters are erased at runtime.
○ erasedValue returns a “fake” literal value that can be used for inline matching
○ It cannot be be used at runtime, only at compile time!
21. erasedValue example
inline def describe[T]: String = inline erasedValue[T] match {
case _: Int => "This is an Int"
case _: String => "This is a String"
case _: List[?] => "This is a List"
case _ => "Unknown type"
}
val intDesc = describe[Int] // "This is an Int"
val strDesc = describe[String] // "This is a String"
val listDesc = describe[List[Int]] // "This is a List"
22. Mirrors
● Mirrors provides type level information about:
○ Product types: Case classes.
○ Sum types: Sealed traits, enums.
23. Mirrors
case class Person(name: String, age: Int)
val mirror = summon[Mirror.ProductOf[Person]]
type Labels = mirror.MirroredElemLabels // ("name", "age")
type Types = mirror.MirroredElemTypes // (String, Int)
val label = constValue[mirror.MirroredLabel] // "Person"
val jose: Person = mirror.fromTuple(("José", 37))
val aTuple: (String, Int) = Tuple.fromProductTyped(jose) // ("José", 37)
24. Type class derivation
● Automatically generate given instances for type classes which satisfy some simple
conditions.
● Keyword in Scala: derives.
● Implementation using derived method.
case class Car(brand: String, year: Int, isNew: Boolean) derives Encoder
inline def derived[E](using m: Mirror.Of[E]): Encoder[E] = ???
● Goal: being able to derive case classes and enums automatically deriving Encoder!
26. Real life applications
● PureConfig
import pureconfig._
import pureconfig.generic.derivation.default._
import
pureconfig.generic.derivation.EnumConfigReader
sealed trait AnimalConf derives ConfigReader
case class DogConf(age: Int) extends AnimalConf
case class BirdConf(canFly: Boolean) extends
AnimalConf
ConfigSource.string("{ type: dog-conf, age: 4
}").load[AnimalConf]
import pureconfig.generic.derivation.EnumConfigReader
enum Season derives EnumConfigReader {
case Spring, Summer, Autumn, Winter
}
case class MyConf(list: List[Season]) derives ConfigReader
ConfigSource.string("{ list: [spring, summer, autumn,
winter] }").load[MyConf]
27. Real life applications
● uPickle
case class Dog(name: String, age: Int) derives ReadWriter
upickle.default.write(Dog("Ball", 2)) // """{"name":"Ball","age":2}"""
upickle.default.read[Dog]("""{"name":"Ball","age":2}""") // Dog("Ball", 2)
sealed trait Animal derives ReadWriter
case class Person(name: String, address: String, age: Int = 20) extends Animal
upickle.default.write(Person("Peter", "Ave 10"))
// """{"$type":"Person","name":"Peter","address":"Ave 10"}"""
upickle.default.read[Animal]("""{"$type":"Person","name":"Peter","address":"Ave 10"}""")
// Person("Peter", "Ave 10")
28. Further material
● Code for the presentation
https://github.com/jospint/scala-3-type-class-derivation
● RockTheJVM: Scala Macros and Metaprogramming
https://rockthejvm.com/courses/scala-macros-and-metaprogramming
● RockTheJVM: Type classes
https://www.youtube.com/watch?v=bupBZKJT0EA
● Riskified Tech: Type class Derivation in Scala 3
https://medium.com/riskified-technology/type-class-derivation-in-scala-3-ba3c7c41d3ef
● Scala 3 Reference: Type class Derivation
https://docs.scala-lang.org/scala3/reference/contextual/derivation.html