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)
