Summary
This incident revolves around unexpected differences in how Scala 3 infers types when calling contains on Set. Although the API signatures appear straightforward, Scala’s type inference and literal typing rules create surprising behavior. The result: two expressions that look identical do not compile the same way.
Root Cause
The root cause is a combination of:
- Literal type inference:
Set(1, 2, 3)is inferred asSet[Int], but only in certain contexts. - Polymorphic vs. monomorphic
contains:Seq.containsis widened:def contains[A1 >: A](elem: A1): BooleanSet.containsis strict:def contains(elem: A): Boolean
- Contextual type inference:
- In inline expressions, Scala may infer a wider element type (e.g.,
Int | String) before type-checkingcontains. - In val bindings, Scala commits to a concrete type earlier, preventing the same widening.
- In inline expressions, Scala may infer a wider element type (e.g.,
Why This Happens in Real Systems
Real compilers must balance type safety, performance, and ergonomics. This leads to:
- Early vs. late type commitment depending on syntactic context
- Literal widening rules that differ between inline expressions and stored values
- Overloaded or polymorphic methods interacting with inferred union types
- Collections with different variance and method signatures producing inconsistent behavior
These are not bugs—they are consequences of complex, interacting design choices.
Real-World Impact
This kind of behavior can cause:
- Confusing compile errors that appear inconsistent
- Unexpected widening of inferred types
- Silent acceptance of obviously-wrong code (e.g.,
Set(1,2,3).contains("b")) - Hard-to-debug type mismatches when refactoring from inline expressions to
valbindings
Example or Code (if necessary and relevant)
// Case A: Does not compile
Set[Int](1, 2, 3).contains("b")
// Case B: Compiles (type inferred as Set[Int | String])
Set(1, 2, 3).contains("b")
// Case C: Does not compile (type fixed as Set[Int])
val set = Set(1, 2, 3)
set.contains("b")
How Senior Engineers Fix It
Experienced engineers typically:
- Force explicit types when inference becomes misleading:
val set: Set[Int] = Set(1, 2, 3) - Avoid relying on inferred union types in collection literals
- Use typed constructors (
Set[Int](...)) to prevent accidental widening - Add compile-time checks or linters to catch suspicious
containscalls - Refactor code to make type expectations explicit, especially in APIs
Why Juniors Miss It
Juniors often miss this issue because:
- They assume type inference is consistent across all contexts
- They expect literal collections to behave the same as typed ones
- They do not yet recognize when Scala widens types (e.g., to unions)
- They assume
containsbehaves uniformly across all collections - They rarely inspect the inferred type of intermediate expressions
The surprising behavior is not a lack of skill—it’s a natural consequence of Scala’s powerful but intricate type system.