Skip to main content
Scala: Contextual Abstractions
  1. Posts/

Scala: Contextual Abstractions

·375 words·2 mins·
Roman
Author
Roman
Photographer with MSci in Computer Science and a Home Lab obsession
Table of Contents

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 above

Type 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 scope

Synthesizing 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