Scala Implicits, Type Classes, and Extension Methods

TT

Scala Implicits, Type Classes, and Extension Methods

Implicits are one of Scala's most powerful — and most debated — features. They enable type classes, extension methods, and contextual abstractions. Scala 3 redesigns them with clearer syntax (given/using), but the concepts are the same. This module covers both Scala 2 and Scala 3 approaches.

Implicit Parameters (Scala 2)

An implicit parameter is filled in automatically by the compiler if a matching implicit value is in scope:

scala
def greet(name: String)(implicit greeting: String): String =
  s"$greeting, $name!"

implicit val defaultGreeting: String = "Hello"

println(greet("Alice"))           // Hello, Alice!
println(greet("Bob")("Hi there")) // Hi there, Bob! — explicit override

The compiler looks for an implicit of the required type in the local scope, enclosing scopes, and companion objects.

given/using (Scala 3)

Scala 3 replaces implicit val/implicit def with given and implicit parameter with using:

scala
// Scala 3
def greet(name: String)(using greeting: String): String =
  s"$greeting, $name!"

given defaultGreeting: String = "Hello"

println(greet("Alice"))  // Hello, Alice!

The behavior is identical, but the syntax is more explicit and easier to understand.

Type Classes: Ad-Hoc Polymorphism

A type class is a trait that defines behavior that can be provided for any type without modifying that type. This is also called ad-hoc polymorphism.

Defining a Type Class

scala
// The type class
trait Printable[A] {
  def print(value: A): String
}

// Type class instances for specific types
implicit val intPrintable: Printable[Int] = (value: Int) => s"Int: $value"
implicit val stringPrintable: Printable[String] = (value: String) => s"String: \"$value\""

// A function that uses the type class
def printValue[A](value: A)(implicit p: Printable[A]): String = p.print(value)

println(printValue(42))       // Int: 42
println(printValue("hello"))  // String: "hello"

Scala 3 Type Class Syntax

scala
// Scala 3
trait Printable[A]:
  def print(value: A): String

given Printable[Int] with
  def print(value: Int): String = s"Int: $value"

given Printable[String] with
  def print(value: String): String = s"String: \"$value\""

def printValue[A](value: A)(using p: Printable[A]): String = p.print(value)

Type Class Derivation and Composition

Type classes can be composed — if you can print A and B, you can print a pair (A, B):

scala
implicit def pairPrintable[A, B](
  implicit pa: Printable[A],
           pb: Printable[B]
): Printable[(A, B)] =
  (pair: (A, B)) => s"(${pa.print(pair._1)}, ${pb.print(pair._2)})"

println(printValue((42, "hello")))  // (Int: 42, String: "hello")

The compiler automatically constructs the Printable[(Int, String)] instance by combining the known instances for Int and String.

Extension Methods (Scala 2: Implicit Classes)

Extension methods add new methods to existing types without modifying them:

scala
// Scala 2: implicit class
implicit class RichInt(val n: Int) extends AnyVal {
  def isEven: Boolean = n % 2 == 0
  def factorial: Int = (1 to n).product
}

println(5.isEven)     // false
println(5.factorial)  // 120

The compiler converts 5.isEven to new RichInt(5).isEven. Extending AnyVal avoids the object allocation (value class optimization).

Scala 3 Extension Methods

scala
// Scala 3
extension (n: Int)
  def isEven: Boolean = n % 2 == 0
  def factorial: Int = (1 to n).product

println(5.isEven)     // false
println(5.factorial)  // 120

Implicit Conversions

Implicit conversions automatically convert one type to another:

scala
case class Celsius(degrees: Double)
case class Fahrenheit(degrees: Double)

implicit def celsiusToFahrenheit(c: Celsius): Fahrenheit =
  Fahrenheit(c.degrees * 9.0/5.0 + 32)

def printFahrenheit(f: Fahrenheit): Unit =
  println(s"${f.degrees}°F")

printFahrenheit(Celsius(100))  // 212.0°F — auto-converted

Implicit conversions are powerful but can make code hard to follow. Use them sparingly and prefer extension methods when you just want to add methods.

The Ordering Type Class

Scala's standard library uses type classes extensively. Ordering[A] defines comparison for type A:

scala
case class Student(name: String, grade: Int)

implicit val studentOrdering: Ordering[Student] =
  Ordering.by(s => (-s.grade, s.name))  // sort by grade desc, then name asc

val students = List(
  Student("Bob", 90),
  Student("Alice", 95),
  Student("Carol", 90)
)

println(students.sorted)
// List(Student(Alice,95), Student(Bob,90), Student(Carol,90))

Frequently Asked Questions

Q: What is the difference between implicits in Scala 2 and given/using in Scala 3? They are the same concept with cleaner syntax. In Scala 2, implicit val declares a value that can be injected, and implicit in a parameter list marks parameters to be injected. In Scala 3, given replaces implicit val/def and using replaces implicit in parameter lists. Scala 3 also makes implicit conversions opt-in (you must import scala.language.implicitConversions), reducing accidental usage.

Q: What is a type class and why is it useful? A type class defines an interface (behavior) that can be implemented for any type after the fact — even types you don't own, like Int or third-party classes. This is called retroactive extension. Unlike inheritance, type classes don't require modifying the original type. The Ordering type class lets you sort any type, the Show type class lets you stringify any type, and so on — all without touching the original class definition.

Q: When should I use an implicit conversion vs an extension method? Prefer extension methods (implicit classes in Scala 2) when you want to add methods to an existing type — they're safer and more readable. Use implicit conversions only when you genuinely need one type to be automatically usable where another is expected, such as a domain-specific numeric type. Implicit conversions that change semantics silently are a common source of bugs.


Part of Scala Mastery Course — Module 14 of 22.