Contextual Abstractions in Scala 3: A cleaner approach to implicits
Explore Scala 3's redesigned contextual abstractions - given, using and summon. How they improve upon Scala 2 implicits for a better developer experience.
If you’ve worked with Scala 2, you’ve likely encountered the implicit keyword. While powerful,
implicits have earned a reputation for being “magical” - code that works in mysterious ways,
making debugging a challenge and leaving newcomers puzzled. Scala 3 addresses these concerns
with a complete redesign called Contextual Abstractions.
In this post, we’ll explore Scala 3’s new approach using practical examples from a stock trading domain.
You’ll learn about given, using, summon, and Conversion - and how they make your code
more explicit, readable, and maintainable.
The Problem with Scala 2 Implicits
In Scala 2, the implicit keyword wore too many hats. It was used for:
- Implicit parameters (dependency injection)
- Implicit conversions (type coercion)
- Implicit classes (extension methods)
- Type class instances
This overloading made code hard to reason about. Consider this Scala 2 stock trading example:
// Scala 2 - The implicit chaos
object TradingApp {
case class Stock(symbol: String, price: BigDecimal)
case class USD(amount: BigDecimal)
case class EUR(amount: BigDecimal)
// Implicit conversion - converts USD to EUR silently
implicit def usdToEur(usd: USD): EUR = EUR(usd.amount * 0.85)
// Implicit parameter - trading context
implicit val tradingContext: TradingContext = new TradingContext {
def marketOpen: Boolean = true
}
// Implicit class - extension methods
implicit class StockOps(stock: Stock) {
def isAffordable(budget: USD)(implicit ctx: TradingContext): Boolean =
ctx.marketOpen && stock.price <= budget.amount
}
def executeOrder(stock: Stock)(implicit ctx: TradingContext): Unit = {
// Where does ctx come from? What conversions might happen?
println(s"Executing order for ${stock.symbol}")
}
}
Looking at executeOrder, can you immediately tell:
- Where
TradingContextcomes from? - What implicit conversions might be triggered?
- Which implicit class methods are available on
Stock?
The answer requires hunting through the codebase. This is the problem Scala 3 solves.
Given Instances
The given keyword in Scala 3 replaces implicit val and implicit object. It’s used to define
canonical values of a type - values that should be automatically provided when needed.
Let’s define our stock trading domain types and create given instances:
// Domain types
case class Stock(symbol: String, name: String, price: BigDecimal, marketCap: Long):
override def toString: String = s"$symbol ($name) @ $$${price}"
enum OrderType:
case Buy, Sell
case class Order(stockSymbol: String, quantity: Int, orderType: OrderType)
case class USD(amount: BigDecimal):
override def toString: String = s"$$${amount} USD"
case class EUR(amount: BigDecimal):
override def toString: String = s"€${amount} EUR"
Now, let’s create a given instance for sorting stocks by price:
// Scala 3 - given instance
given stockByPrice: Ordering[Stock] = Ordering.by(_.price)
// Usage
val stocks = List(
Stock("AAPL", "Apple", 178.50, 2800000000000L),
Stock("MSFT", "Microsoft", 378.91, 2810000000000L),
Stock("GOOGL", "Alphabet", 141.80, 1780000000000L)
)
stocks.sorted // Automatically uses stockByPrice
// Result: List(GOOGL at 141.80, AAPL at 178.50, MSFT at 378.91)
Compare this to Scala 2:
// Scala 2
implicit val stockByPrice: Ordering[Stock] = Ordering.by(_.price)
The difference seems minor, but given clearly communicates intent: “This is a canonical value
that should be available in context.” You can also define anonymous given instances when the name
isn’t important:
// Anonymous given - when you don't need to reference it by name
given Ordering[Stock] = Ordering.by(_.price)
Multiple Given Instances
What if you want to sort stocks by different criteria? To avoid ambiguity errors, keep only one
default given in scope and place alternatives in an object:
// Default ordering (only one given in scope to avoid ambiguity)
given stockByPrice: Ordering[Stock] = Ordering.by(_.price)
// Alternative orderings kept in an object - import explicitly when needed
object StockOrderings:
given byMarketCap: Ordering[Stock] = Ordering.by(_.marketCap)
given bySymbol: Ordering[Stock] = Ordering.by(_.symbol)
// Explicitly choose which ordering to use
stocks.sorted(using StockOrderings.byMarketCap)
This pattern avoids the “Ambiguous given instances” error that occurs when multiple givens of the same type are in scope.
Using Clauses
The using keyword replaces implicit parameters. It makes the contract explicit: “This function
requires a contextual value to be available.”
Let’s create a trading context that our functions will depend on:
trait TradingContext:
def marketOpen: Boolean
def defaultCurrency: String
def maxOrderSize: Int
trait PricingStrategy:
def calculateTotalCost(stock: Stock, quantity: Int): BigDecimal
Now define given instances and functions that use them:
// Define our contexts as given instances
given defaultContext: TradingContext with
def marketOpen: Boolean = true
def defaultCurrency: String = "USD"
def maxOrderSize: Int = 10000
given standardPricing: PricingStrategy with
def calculateTotalCost(stock: Stock, quantity: Int): BigDecimal =
stock.price * quantity * BigDecimal("1.001") // 0.1% trading fee
// Functions that require context via 'using'
def validateOrder(order: Order)(using ctx: TradingContext): Boolean =
ctx.marketOpen && order.quantity <= ctx.maxOrderSize
def executeOrder(order: Order)(using ctx: TradingContext, pricing: PricingStrategy): Unit =
if validateOrder(order) then
println(s"Order validated in ${ctx.defaultCurrency}")
else
println(s"Order validation failed")
The using clause clearly documents what contextual dependencies a function has. When you call
executeOrder, the compiler automatically provides the given instances:
val order = Order("AAPL", 100, OrderType.Buy)
// Compiler automatically supplies TradingContext and PricingStrategy
executeOrder(order)
// Or explicitly pass a different context
executeOrder(order)(using customContext, customPricing)
Context Bounds (Shorthand Syntax)
For common patterns like type classes, Scala 3 provides a shorthand using context bounds:
// These are equivalent:
def sortStocks[T](items: List[T])(using ord: Ordering[T]): List[T] = items.sorted
def sortStocks[T: Ordering](items: List[T]): List[T] = items.sorted
The [T: Ordering] syntax means “T must have an Ordering instance available in context.”
Summoning Instances
Sometimes you need to retrieve a contextual value explicitly within your code. In Scala 2,
you’d use implicitly[T]. Scala 3 replaces this with summon[T] - a clearer name that
describes exactly what it does.
trait RiskCalculator:
def assessRisk(order: Order): String
given defaultRiskCalculator: RiskCalculator with
def assessRisk(order: Order): String =
if order.quantity > 1000 then "HIGH"
else if order.quantity > 100 then "MEDIUM"
else "LOW"
Now use summon to retrieve the instance:
def processOrder(order: Order)(using TradingContext): Unit =
// Summon the RiskCalculator from context
val riskCalc = summon[RiskCalculator]
val riskLevel = riskCalc.assessRisk(order)
println(s"Risk level: $riskLevel")
Compare with Scala 2:
// Scala 2
val riskCalc = implicitly[RiskCalculator]
// Scala 3
val riskCalc = summon[RiskCalculator]
The summon function is more descriptive - you’re “summoning” a value from the contextual
environment rather than asking for something “implicitly.”
Practical Use Case: Summoning in Generic Code
summon is particularly useful in generic code where you need to access type class instances:
def getOrdering[T: Ordering]: Ordering[T] = summon[Ordering[T]]
// Use it to create custom comparisons
val priceOrdering = getOrdering[Stock](using stockByPrice)
Contextual Conversions with Conversion[-T, +U]
Implicit conversions in Scala 2 were powerful but dangerous. They could silently convert
types in unexpected ways, leading to subtle bugs. Scala 3 introduces Conversion[-T, +U]
as a safer, more explicit alternative.
The Problem with Scala 2 Implicit Conversions
// Scala 2 - Dangerous implicit conversion
case class USD(amount: BigDecimal)
case class EUR(amount: BigDecimal)
implicit def usdToEur(usd: USD): EUR = EUR(usd.amount * 0.85)
def payInEuros(amount: EUR): Unit = println(s"Paying ${amount.amount} EUR")
// This compiles silently - USD is converted to EUR without warning!
payInEuros(USD(100))
This silent conversion can cause real problems in a trading system where currency accuracy is critical.
Scala 3’s Safer Approach
Scala 3 requires you to explicitly opt into conversions using scala.Conversion:
import scala.language.implicitConversions
case class USD(amount: BigDecimal)
case class EUR(amount: BigDecimal)
// Explicit conversion with exchange rate
given usdToEur: Conversion[USD, EUR] with
def apply(usd: USD): EUR = EUR(usd.amount * BigDecimal("0.85"))
The key differences:
- You must import
scala.language.implicitConversionsto enable conversions - The
Conversiontrait makes the intent explicit - Conversions are defined as given instances, making them discoverable
Using Conversions in Practice
def processEuroPayment(amount: EUR): Unit =
println(s"Processing payment: ${amount.amount} EUR")
// With the given Conversion in scope:
val dollars = USD(BigDecimal("100.00"))
processEuroPayment(dollars) // Converted automatically: Processing payment: 85.00 EUR
Bidirectional Conversions
For a complete currency conversion system:
object CurrencyConversions:
private val usdToEurRate = BigDecimal("0.85")
private val eurToUsdRate = BigDecimal("1.18")
given Conversion[USD, EUR] =
usd => EUR((usd.amount * usdToEurRate).setScale(2, BigDecimal.RoundingMode.HALF_UP))
given Conversion[EUR, USD] =
eur => USD((eur.amount * eurToUsdRate).setScale(2, BigDecimal.RoundingMode.HALF_UP))
// Usage
import CurrencyConversions.given
val inDollars: USD = USD(100)
val inEuros: EUR = inDollars // Automatic conversion
val backToDollars: USD = inEuros // And back
Type-Safe Order Conversions
Another practical example - converting order requests to executed trades:
import java.time.Instant
case class OrderRequest(symbol: String, quantity: Int, orderType: OrderType)
case class ExecutedOrder(
symbol: String,
quantity: Int,
orderType: OrderType,
executedAt: Instant,
status: String
)
given Conversion[OrderRequest, ExecutedOrder] with
def apply(req: OrderRequest): ExecutedOrder =
ExecutedOrder(
symbol = req.symbol,
quantity = req.quantity,
orderType = req.orderType,
executedAt = Instant.now(),
status = "EXECUTED"
)
def recordTrade(executed: ExecutedOrder): Unit =
println(s"Recorded: ${executed.symbol} - ${executed.status}")
// Convert and record in one step
val request = OrderRequest("AAPL", 50, OrderType.Buy)
recordTrade(request) // Automatic conversion to ExecutedOrder
Developer Experience Improvements
Scala 3’s contextual abstractions aren’t just about new syntax - they fundamentally improve the developer experience in several ways.
Clearer Intent and Separation of Concerns
Each concept now has its own keyword:
| Purpose | Scala 2 | Scala 3 |
|---|---|---|
| Canonical values | implicit val/object | given |
| Context parameters | implicit parameter | using clause |
| Retrieve from context | implicitly[T] | summon[T] |
| Type conversions | implicit def | given Conversion[A, B] |
| Extension methods | implicit class | extension |
This separation makes code self-documenting. When you see given, you know it’s
defining a canonical instance. When you see using, you know a context dependency
is required.
Better Compiler Error Messages
Scala 3’s compiler provides significantly better error messages for contextual abstractions. When a required given instance is missing:
def placeOrder(order: Order)(using ctx: TradingContext): Unit = ???
placeOrder(Order("AAPL", 100, OrderType.Buy))
// Error: No given instance of type TradingContext was found for parameter ctx
The error clearly states what’s missing and where it’s needed - no more cryptic “implicit not found” messages.
Improved IDE Support and Discoverability
Modern IDEs like IntelliJ IDEA and Metals can now:
- Show which given instances are in scope
- Navigate directly to given definitions
- Suggest imports for missing given instances
- Display parameter info showing
usingrequirements
This makes contextual abstractions discoverable rather than hidden.
Migration Path from Scala 2
Scala 3 provides a smooth migration path. The old implicit syntax still works but generates
deprecation warnings. You can migrate gradually:
// This still works in Scala 3 (with warnings)
implicit val oldStyle: Ordering[Stock] = Ordering.by(_.price)
// Recommended Scala 3 style
given newStyle: Ordering[Stock] = Ordering.by(_.price)
Summary
Scala 3’s contextual abstractions represent a significant improvement over Scala 2 implicits:
given- Defines canonical type class instances with clear intentusing- Declares context dependencies explicitly in function signaturessummon- Retrieves contextual values with a descriptive nameConversion- Provides type-safe, opt-in implicit conversions
These changes make Scala code more readable, maintainable, and easier to debug. The stock trading examples we explored demonstrate how these features work together in real-world scenarios.
If you’re migrating from Scala 2, the transition is straightforward. Start by replacing
implicit val with given and implicit parameters with using clauses. Your code
will become clearer.
For more details, check out the official Scala 3 documentation on Contextual Abstractions.
Update [2025-12-15]: Code samples were updated to match latest scala 3 version.