This week I’m looking into Contextual Abstractions as preparation for the ZIO course | Rock the JVM.
Contextual Abstractions #
Given/Using Combo #
The given keyword defines values that can be automatically passed to functions expecting them via using.
// Basic given/using pattern
def increment(x: Int)(using amount: Int): Int = x + amount
// Compiler will look for the
// type of using and replace it with given of the same type
given defaultAmount: Int = 10
// Now instead of val twelve = increment(2)(10)
val twelve = increment(2)
// the compiler will search for a given Int in the scope
def multiply(x: Int)(using factor: Int): Int = x * factor
val aHundred = multiply(10) // uses the defaultAmount given aboveType Class Pattern #
The type class pattern uses given instances to provide implementations for generic behaviour.
// more complex use case
trait Combiner[A] {
def combine(x: A, y: A): A
def empty: A
}
def combineAll[A](values: List[A])(using combiner: Combiner[A]): A = {
values.foldLeft(combiner.empty)(combiner.combine)
}
given intCombiner: Combiner[Int] with {
override def combine(x: Int, y: Int): Int = x + y
override def empty: Int = 0
}
val numbers = (1 to 10).toList
val sum10 = combineAll(numbers) // uses the given intCombiner
combineAll(List("apple", "banana"))
// would not compile - no given Combiner[String] in scopeSynthesizing Given Instances #
You can synthesize new given instances from existing ones.
// Synthesize given instances
given optionCombiner[T](using combiner: Combiner[T]): Combiner[Option[T]] with {
override def empty = Some(combiner.empty)
override def combine(x: Option[T], y: Option[T]) = for {
vx <- x
vy <- y
} yield combiner.combine(vx, vy)
}
val sumOption: Option[Int] = combineAll(List(Some(1), None, Some(2)))Extension Methods #
Extension methods allow you to add new methods to existing types without modifying their source code.
// Basic extension method
case class Person(name: String) {
def greet(): String = s"Hi, my name is $name"
}
extension (name: String)
def greet(): String = Person(name).greet()
val alicesGreeting = "Alice".greet() // "Hi, my name is Alice"Generic Extensions #
Extension methods can be generic, working with parameterized types. They can also use given instances, combining with the type class pattern.
// generic extension
extension [T](list: List[T])
def reduceAll(using combiner: Combiner[T]): T =
list.foldLeft(combiner.empty)(combiner.combine)
val sum10V2 = numbers.reduceAll // uses the intCombiner given instance