Scala同类型双隐式Monoid实例:编译器如何选择对应实现?
Monoid Instances for the Same Money Type Great question! This is a super common point of confusion when working with Scala typeclasses like Monoid—especially when you want multiple instances for the same underlying type. Let’s break down exactly how the compiler resolves which implicit to use, using your Money example as a guide.
First, a Quick Recap of Implicit Resolution Rules
Scala’s compiler follows strict, predictable rules to find implicit values, prioritizing them in this order:
- Local scope: Implicit values defined directly in the current method, code block, or enclosing class/object.
- Imported implicits: Values brought into scope via
importstatements. - Companion objects: Implicit values defined in the companion object of the target type (e.g.,
Money’s companion) or the typeclass’s companion (e.g.,Monoid’s companion).
The key rule here: the compiler only looks for an implicit value that exactly matches the type required by the implicit parameter. If multiple matching implicits are in scope, you’ll get an ambiguous implicit error—so your code needs to structure instances to avoid this conflict.
Applying This to Your Money Monoid Instances
You mentioned two Monoid[Money] instances: one for addition (MoneyAdditionMonoid) and one for comparison (MoneyCompareMonoid). Let’s fill in the missing code context to make this concrete, then walk through how the compiler picks the right instance.
Example Full Code (With Common Patterns)
case class Money(amount: BigDecimal, currency: String) trait Monoid[T] { def zero: T def op(t1: T, t2: T): T } // Addition monoid: sums amounts (assuming matching currencies for simplicity) object MoneyAdditionMonoid extends Monoid[Money] { def zero: Money = Money(BigDecimal(0), "USD") def op(t1: Money, t2: Money): Money = Money(t1.amount + t2.amount, t1.currency) } // Comparison monoid: keeps the maximum debit (smallest negative amount) object MoneyCompareMonoid extends Monoid[Money] { def zero: Money = Money(BigDecimal(Int.MaxValue), "USD") // Identity for max: larger than any possible debit def op(t1: Money, t2: Money): Money = if (t1.amount < t2.amount) t1 else t2 } // Your functions def sumBalances(transactions: List[Money])(implicit m: Monoid[Money]): Money = transactions.foldLeft(m.zero)(m.op) def maxDebitOnDay(debits: List[Money])(implicit m: Monoid[Money]): Money = debits.foldLeft(m.zero)(m.op)
How the Compiler Picks the Right Instance
If both instances are marked as implicit and live in the same scope, you’ll get an ambiguous implicit error—Scala can’t guess your intent! To fix this, you’ll use one of two standard patterns:
1. Scope Instances to Specific Contexts
Wrap each implicit instance in a separate object, then import only the one you need when calling the function:
// Group monoids into context-specific objects object MoneyMonoids { object Addition { implicit val moneyAdditionMonoid: Monoid[Money] = MoneyAdditionMonoid } object Comparison { implicit val moneyCompareMonoid: Monoid[Money] = MoneyCompareMonoid } } // When summing balances: import the addition monoid import MoneyMonoids.Addition._ val totalBalance = sumBalances(List(Money(100, "USD"), Money(200, "USD"))) // Uses addition instance // When finding max debit: import the comparison monoid import MoneyMonoids.Comparison._ val largestDebit = maxDebitOnDay(List(Money(-50, "USD"), Money(-100, "USD"))) // Uses comparison instance
Here, only one implicit Monoid[Money] is in scope at a time, so the compiler picks that instance without ambiguity.
2. Use Tagged Types (Type-Safe Enforcement)
For stricter compile-time safety, tag the Money type to distinguish between "addable" and "comparable" money (using lightweight AnyVal tagging):
// Tagged wrapper types case class AddableMoney(underlying: Money) extends AnyVal case class ComparableMoney(underlying: Money) extends AnyVal // Monoid instances for the tagged types implicit val addableMoneyMonoid: Monoid[AddableMoney] = new Monoid[AddableMoney] { def zero: AddableMoney = AddableMoney(Money(BigDecimal(0), "USD")) def op(t1: AddableMoney, t2: AddableMoney): AddableMoney = AddableMoney(Money(t1.underlying.amount + t2.underlying.amount, t1.underlying.currency)) } implicit val comparableMoneyMonoid: Monoid[ComparableMoney] = new Monoid[ComparableMoney] { def zero: ComparableMoney = ComparableMoney(Money(BigDecimal(Int.MaxValue), "USD")) def op(t1: ComparableMoney, t2: ComparableMoney): ComparableMoney = ComparableMoney(if (t1.underlying.amount < t2.underlying.amount) t1.underlying else t2.underlying) } // Updated functions with tagged type parameters def sumBalances(transactions: List[AddableMoney])(implicit m: Monoid[AddableMoney]): AddableMoney = transactions.foldLeft(m.zero)(m.op) def maxDebitOnDay(debits: List[ComparableMoney])(implicit m: Monoid[ComparableMoney]): ComparableMoney = debits.foldLeft(m.zero)(m.op)
Now you can’t accidentally pass a ComparableMoney to sumBalances (or vice versa). The compiler will automatically pick the Monoid instance that exactly matches the tagged type parameter.
内容的提问来源于stack exchange,提问作者Alexandr

