Scala Implicits

Implicits let the compiler automatically supply values, conversions, or parameters that you would otherwise write explicitly. When you mark something as implicit (or given in Scala 3), the compiler finds and injects it where needed. This removes repetitive boilerplate and enables powerful patterns like type classes and extension methods.

Implicit Parameters

An implicit parameter is filled in automatically by the compiler when you omit it:

// Scala 3 style: 'using' keyword
def greet(name: String)(using prefix: String): String =
  s"$prefix, $name!"

given myPrefix: String = "Hello"   // 'given' makes it available implicitly

println(greet("Alice"))          // Hello, Alice!  (prefix supplied automatically)
println(greet("Bob"))            // Hello, Bob!
println(greet("Carol")(using "Hey"))  // Hey, Carol!  (explicit override)

given / using in Scala 3

// Define a given instance
given ordering: Ordering[String] = Ordering.by(_.length)

def sortByRule[A](items: List[A])(using ord: Ordering[A]): List[A] =
  items.sorted

val words = List("banana", "fig", "apple", "kiwi")
println(sortByRule(words))  // List(fig, kiwi, apple, banana)  — sorted by length

Extension Methods (Scala 3)

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

extension (n: Int)
  def isEven: Boolean = n % 2 == 0
  def isOdd: Boolean  = !n.isEven
  def times(f: () => Unit): Unit = (1 to n).foreach(_ => f())
  def squared: Int    = n * n

println(7.isOdd)     // true
println(8.isEven)    // true
println(5.squared)   // 25
3.times(() => println("Hello!"))
// Hello!
// Hello!
// Hello!

Extension on String

extension (s: String)
  def wordCount: Int      = s.split("\\s+").length
  def isPalindrome: Boolean = s == s.reverse
  def toTitleCase: String =
    s.split(" ").map(w => w.head.toUpper + w.tail.toLowerCase).mkString(" ")

println("hello world scala".wordCount)   // 3
println("racecar".isPalindrome)          // true
println("the quick brown fox".toTitleCase) // The Quick Brown Fox

Implicit Conversions (Use Sparingly)

// Scala 2 style implicit conversion
implicit def intToString(n: Int): String = n.toString

// Scala 3 requires explicit import
import scala.language.implicitConversions
given Conversion[Int, String] = _.toString

val s: String = 42   // compiler inserts conversion automatically
println(s)   // "42"

Implicit conversions can make code hard to follow. Use them sparingly and only in well-documented library code. Extension methods are preferred for adding methods; using parameters are preferred for dependency injection.

Type Class Pattern with given/using

// Define the type class
trait Show[A]:
  def show(value: A): String

// Provide instances for specific types
given Show[Int] with
  def show(n: Int): String = s"Int($n)"

given Show[String] with
  def show(s: String): String = s"Str($s)"

given Show[Boolean] with
  def show(b: Boolean): String = if b then "yes" else "no"

// Generic function that works with any Show instance
def display[A](value: A)(using s: Show[A]): String = s.show(value)

println(display(42))      // Int(42)
println(display("hello")) // Str(hello)
println(display(true))    // yes

summon — Accessing a given

val intShow = summon[Show[Int]]
println(intShow.show(99))   // Int(99)

// Like implicitly in Scala 2
// val intShow2 = implicitly[Show[Int]]

Implicit Search Rules


When the compiler looks for a given/implicit, it searches:
  1. Current scope
  2. Imported scopes
  3. Companion objects of types involved
  4. Inherited scopes

Priority: explicit > imported > companion object
If ambiguous → compile error
If missing   → compile error

Practical: Configurable Logging

case class LogConfig(level: String, prefix: String)

given defaultConfig: LogConfig = LogConfig("INFO", "[App]")

def log(message: String)(using config: LogConfig): Unit =
  println(s"${config.prefix} [${config.level}] $message")

log("Server started")   // [App] [INFO] Server started
log("User logged in")   // [App] [INFO] User logged in

// Override for a specific scope
{
  given debugConfig: LogConfig = LogConfig("DEBUG", "[Debug]")
  log("Entering function")   // [Debug] [DEBUG] Entering function
}
// Outside block — back to defaultConfig
log("Back to normal")   // [App] [INFO] Back to normal

Leave a Comment

Your email address will not be published. Required fields are marked *