Scala Context Parameters

Context parameters (introduced in Scala 3) are the modern replacement for implicit parameters. They let you pass values automatically based on what is available in the current scope, without writing them explicitly at every call site. The compiler finds and injects the right value, keeping function signatures clean while making dependencies visible and trackable.

The using / given Pair


given  →  "I am making this value available to the compiler"
using  →  "I need a value of this type from the compiler"
// Declare a context parameter with 'using'
def greet(name: String)(using lang: String): String =
  lang match
    case "en" => s"Hello, $name!"
    case "es" => s"¡Hola, $name!"
    case "hi" => s"नमस्ते, $name!"
    case _    => s"Hi, $name!"

// Provide a given instance
given String = "en"

println(greet("Alice"))        // Hello, Alice!   (given "en" injected)
println(greet("Bob")(using "es"))  // ¡Hola, Bob!   (explicit override)

Named given Instances

case class AppConfig(
  dbHost: String,
  dbPort: Int,
  maxConnections: Int,
  timeoutSeconds: Int
)

given productionConfig: AppConfig =
  AppConfig("prod.db.example.com", 5432, 50, 30)

def connectDB()(using config: AppConfig): String =
  s"Connecting to ${config.dbHost}:${config.dbPort} " +
  s"(max=${config.maxConnections}, timeout=${config.timeoutSeconds}s)"

def fetchUsers()(using config: AppConfig): String =
  s"Fetching from ${config.dbHost} with ${config.maxConnections} max connections"

println(connectDB())   // Connecting to prod.db.example.com:5432 (max=50, timeout=30s)
println(fetchUsers())  // Fetching from prod.db.example.com with 50 max connections

Context Parameters vs Regular Parameters


Regular parameter:                  Context parameter:
────────────────────────────────    ────────────────────────────────
def f(x: Int, config: Config)       def f(x: Int)(using config: Config)

Caller must always write it:        Compiler finds it automatically:
f(5, myConfig)                      f(5)   // config injected from scope
f(5, otherConfig)                   f(5)(using otherConfig)  // override

Multiple Context Parameters

trait Logger:
  def log(msg: String): Unit

trait Metrics:
  def record(event: String): Unit

given Logger with
  def log(msg: String): Unit = println(s"[LOG] $msg")

given Metrics with
  def record(event: String): Unit = println(s"[METRIC] $event")

def processOrder(orderId: String)(using logger: Logger, metrics: Metrics): Unit =
  logger.log(s"Processing order $orderId")
  metrics.record("order.processed")
  logger.log(s"Order $orderId complete")

processOrder("ORD-1001")
// [LOG] Processing order ORD-1001
// [METRIC] order.processed
// [LOG] Order ORD-1001 complete

Context Functions (Scala 3)

A context function type A ?=> B is a function that implicitly receives an A from the calling context. This enables concise DSL-style code:

type Configured[T] = AppConfig ?=> T

def getHost: Configured[String] = summon[AppConfig].dbHost
def getPort: Configured[Int]    = summon[AppConfig].dbPort

given AppConfig = AppConfig("api.example.com", 8080, 20, 15)

println(getHost)   // api.example.com
println(getPort)   // 8080

summon — Access a given Directly

given greeting: String = "Namaste"

val g = summon[String]
println(g)   // Namaste

// Useful inside generic functions to access the given
def showContext[A]()(using ev: A): A =
  val value = summon[A]
  value

Context Parameters for Dependency Injection

trait Database:
  def query(sql: String): List[String]

trait Cache:
  def get(key: String): Option[String]
  def set(key: String, value: String): Unit

// Test implementations
given testDB: Database with
  def query(sql: String): List[String] = List(s"row1_for($sql)", s"row2_for($sql)")

given testCache: Cache with
  private val store = scala.collection.mutable.Map[String, String]()
  def get(key: String): Option[String] = store.get(key)
  def set(key: String, value: String): Unit = store(key) = value

def getUserById(id: Int)(using db: Database, cache: Cache): String =
  val cacheKey = s"user:$id"
  cache.get(cacheKey) match
    case Some(cached) =>
      s"[CACHE] $cached"
    case None =>
      val rows = db.query(s"SELECT * FROM users WHERE id=$id")
      val result = rows.headOption.getOrElse("Not found")
      cache.set(cacheKey, result)
      s"[DB] $result"

println(getUserById(1))   // [DB] row1_for(SELECT * FROM users WHERE id=1)
println(getUserById(1))   // [CACHE] row1_for(SELECT * FROM users WHERE id=1)
println(getUserById(2))   // [DB] row1_for(SELECT * FROM users WHERE id=2)

Scope and Priority

given globalConfig: AppConfig = AppConfig("global.host", 80, 10, 5)

def runTask()(using cfg: AppConfig): Unit =
  println(s"Running on ${cfg.dbHost}:${cfg.dbPort}")

runTask()   // Running on global.host:80

// Local given shadows the global one
{
  given localConfig: AppConfig = AppConfig("local.host", 9090, 5, 10)
  runTask()   // Running on local.host:9090
}
runTask()   // Running on global.host:80  (back to global)

Typeclass Summoning Pattern

trait Validator[A]:
  def validate(value: A): Either[String, A]

object Validator:
  def apply[A](using v: Validator[A]): Validator[A] = v

given Validator[String] with
  def validate(s: String): Either[String, String] =
    if s.nonEmpty then Right(s) else Left("String cannot be empty")

given Validator[Int] with
  def validate(n: Int): Either[String, Int] =
    if n >= 0 then Right(n) else Left("Number must be non-negative")

def validated[A](value: A)(using v: Validator[A]): Either[String, A] =
  v.validate(value)

println(validated("hello"))   // Right(hello)
println(validated(""))        // Left(String cannot be empty)
println(validated(42))        // Right(42)
println(validated(-1))        // Left(Number must be non-negative)

Leave a Comment

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