An existential type says: there is some type here, I just will not tell you which. List[?] is a list of something. Array[?] is an array of something. You can ask such a value for its length, iterate it, or hand it to a generic method, but you cannot put an arbitrary value into it, because the element type is hidden and the compiler refuses to guess.
Most Scala developers meet existentials through Java interop, error messages that mention _$1, or a migration to Scala 3 that rejects forSome. This article explains how the compiler reasons about a hidden type, what Scala 3 removed, and the patterns that replace it, ending with a type-safe event-handler registry.
The problem: invariance leaves no common supertype
Start with a mutable container. Array[Int] is invariant in its element type, so it is not an Array[Any]. That rule is what keeps arrays sound: if the assignment were allowed, you could write a String into an array the rest of the program believes holds integers.
Invariance has a cost. Suppose you want a function that prints the length of any array, or a list holding arrays of different element types. Array[Any] does not work, because nothing except an Array[Any] conforms to it. You need a type that means an array of some element type, unknown to me. That is an existential type, and in current Scala you write it with a wildcard:
val ints: Array[Int] = Array(1, 2, 3)
// val anys: Array[Any] = ints // does not compile: Array is invariant
val some: Array[?] = ints // Scala 3 (Scala 2: Array[_])
def length(xs: Array[?]): Int = xs.length
val mixed: List[Array[?]] = List(Array(1, 2), Array("a", "b", "c"))
mixed.map(length) // List(2, 3)Every Array[T] conforms to Array[?], whatever T is. In return, the operations available on the wildcard type are only those that are safe for every possible element type. Reading gives you a value of the upper bound, which is Any here. Writing needs a value of the lower bound, which is Nothing, so in practice you cannot write at all.
Scala 2's two syntaxes: forSome and the wildcard shorthand
Scala 2 had a general existential form, T forSome { type X; ... }, and the wildcard _ as shorthand for its common case. List[_] meant List[X] forSome { type X }. Bounds carried over: List[_ <: Number] meant List[X] forSome { type X <: Number }. The general form could say things wildcards cannot, because one hidden type could appear in several positions:
// Scala 2 only
import scala.language.existentials
type AnyList = List[X] forSome { type X } // same as List[_]
type Paired = (List[X], X => String) forSome { type X } // one hidden X used twice
type Rows = Map[Class[X], X] forSome { type X } // NOT a heterogeneous mapPaired is the interesting one: a list together with a function that can render that list's elements. A wildcard version, (List[_], _ => String), hides two independent types and loses the link. Rows is a classic trap: it reads like a map where each key's class matches its value, but it actually means a map in which one hidden X is shared by every entry. It is not a heterogeneous map at all.
Since Scala 2.10, existentials that cannot be written with wildcards trigger a feature warning unless you import scala.language.existentials.
| You want | Scala 2 | Scala 3 |
|---|---|---|
| Any element type | List[_] | List[?] (_ still accepted, being phased out) |
| Upper-bounded | List[_ <: Number] | List[? <: Number] |
| Lower-bounded | List[_ >: Int] | List[? >: Int] |
| One hidden type used in two places | (List[X], X => String) forSome { type X } | Not expressible as a type; use a packed class or type member |
Java raw type Class | Class[_] | Class[?] |
How the compiler opens an existential: skolems
When you use a value of type Array[?], the compiler does not treat the element type as Any. It invents a fresh, unnamed type, called a skolem, that stands for the specific hidden type of this particular value. Scala 2 prints skolems as _$1, _$2 and so on. Knowing this explains the most confusing error messages in the area:
def swapFirstTwo(xs: Array[?]): Unit =
val tmp = xs(0) // tmp: Any (the skolem widened to its bound)
xs(0) = xs(1) // error: the element type of xs is unknown
xs(1) = tmp // error: Any is not the hidden element typeBoth writes fail because the compiler cannot prove that xs(1) has the same skolem as the slot it is assigned to; reading from the wildcard has already widened it to Any. The fix is to give the hidden type a name by calling a generic method. When you pass an Array[?] to a method expecting Array[A], the compiler binds A to the skolem. This is called capture conversion, and both Scala 2 and Scala 3 perform it:
def swapFirstTwo(xs: Array[?]): Unit = swapTyped(xs) // A := the hidden type
private def swapTyped[A](xs: Array[A]): Unit =
val tmp: A = xs(0)
xs(0) = xs(1)
xs(1) = tmpInside swapTyped the type has a name, so reads and writes line up. This one technique, a public method taking a wildcard and a private generic helper that does the work, resolves most real-world existential errors.
What Scala 3 dropped, and why
Scala 3 removed forSome. The Scala 3 reference lists it under dropped features: general existential types do not fit the DOT calculus that Scala 3's type system is built on, and they interacted badly with type inference and path-dependent types. What remains is the wildcard form, which Scala 3 models as a bounded type argument rather than as a separate kind of type.
Scala 3 also changed the spelling. ? is the wildcard. _ is still accepted for compatibility, but its role as a wildcard is being phased out so that _ can later mean a type lambda placeholder. Two practical consequences follow. First, if you used the kind-projector compiler plugin, its old ? placeholder clashes with Scala 3's wildcard; kind-projector switched to * for that reason, and Scala 3 offers a -Ykind-projector option for cross-built code. Second, Scala 2.13 accepts ? as a wildcard under the -Xsource:3 flag, so cross-built code can use one spelling in both.
Every other use of forSome falls into one of three shapes, each with a direct replacement.
Replacement 1: a type parameter on the method
If the hidden type only matters inside one method, make it a type parameter. def f(x: (List[X], X => String) forSome { type X }) becomes def f[X](xs: List[X], render: X => String). Callers already know the type, so they lose nothing.
Replacement 2: a type member, opened by path
If the value must travel, stored in a collection or returned from a function, you need a value that carries its type with it. A trait with an abstract type member does exactly that, and Scala's path-dependent types let the compiler connect a value to its own hidden type:
trait Show[A]:
def show(a: A): String
trait Packed:
type T
def value: T
def shower: Show[T]
def pack[A](a: A)(using s: Show[A]): Packed = new Packed:
type T = A
val value = a
val shower = s
given Show[Int] = i => s"Int($i)"
given Show[String] = s => s"'$s'"
val bag: List[Packed] = List(pack(42), pack("hi"))
bag.map(p => p.shower.show(p.value)) // List(Int(42), 'hi')Inside the lambda, p.value has type p.T and p.shower accepts p.T, so the call type-checks even though nobody knows what p.T is. This is exactly the Paired existential from earlier, expressed with a type member instead of forSome. The path-dependent types article covers the mechanism in more depth.
Replacement 3: a packed case class with a pattern-bound type
The lightest replacement is an ordinary generic class whose type parameter you hide with a wildcard when storing it, then recover when you use it:
final case class Shown[A](value: A, ev: Show[A]):
def render: String = ev.show(value) // typed inside the class
val items: List[Shown[?]] = List(Shown(42, summon[Show[Int]]), Shown("hi", summon[Show[String]]))
items.map(_.render)
items.map { case s: Shown[a] => s.ev.show(s.value) } // a is bound to the hidden typeTwo things make this work. Methods defined inside Shown see A as a real type, so render is always well typed. And a lowercase type variable in a pattern, Shown[a], binds the hidden type for the duration of the case, the pattern-matching equivalent of capture conversion. No cast is involved.
Worked example: a type-safe event-handler registry
A common real need is a registry that maps event classes to handlers. The naive version stores Map[Class[?], Any => Unit] and casts on every dispatch, which moves type errors from compile time to production. Using the packed-class pattern and a private generic helper gives a version where each handler is typed against its own event class:
import scala.reflect.ClassTag
final case class Handler[E](cls: Class[E], run: E => Unit)
final class Registry:
private var handlers: List[Handler[?]] = Nil
def on[E <: AnyRef](run: E => Unit)(using ct: ClassTag[E]): Unit =
handlers ::= Handler(ct.runtimeClass.asInstanceOf[Class[E]], run)
/** Returns how many handlers accepted the event. */
def dispatch(event: AnyRef): Int =
handlers.count(h => fire(h, event)) // h: Handler[?] is captured as Handler[E]
private def fire[E](h: Handler[E], event: AnyRef): Boolean =
if h.cls.isInstance(event) then
h.run(h.cls.cast(event)) // typed: cast returns E, run takes E
true
else false
final case class OrderPlaced(id: String, cents: Long)
final case class OrderCancelled(id: String)
val r = Registry()
r.on[OrderPlaced](e => println(s"placed ${e.id}"))
r.dispatch(OrderPlaced("A-17", 4_250)) // prints "placed A-17", returns 1Follow the types through one dispatch. handlers is a List[Handler[?]], a list of handlers for some event type each. handlers.count(h => fire(h, event)) passes each Handler[?] to fire[E], and capture conversion binds E to that handler's hidden type. Inside fire, h.cls.cast(event) returns an E and h.run accepts an E, so the compiler checks the link that the naive map could only assume.
Note the E <: AnyRef bound on on. Without it, someone can register on[Int]. The ClassTag[Int] has the primitive class int as its runtime class, and classOf[Int].isInstance(boxedInteger) is false, so the handler silently never fires. The bound turns that run-time surprise into a compile error. The same reasoning is why ClassTag cannot distinguish List[Int] from List[String]: erasure removes type arguments, so key events on concrete case classes, not on generic types.
Java interop: wildcards, raw types and Class
Java's use-site variance maps directly onto Scala wildcards. A Java signature List<? extends Number> appears in Scala as java.util.List[? <: Number]. Raw Java types, such as a legacy API returning plain Class or List, are seen as Class[?] and java.util.List[?].
Failure modes
- Reaching for
Anyinstead of?.List[Array[Any]]rejects everyArray[Int]because of invariance; the wildcard is the type you meant. - Expecting a heterogeneous map from
Map[Class[?], ?]. Two wildcards are two unrelated hidden types. The key's class and the value's type are not linked; use a packed entry class or a typed key. - Leaking a skolem. Returning a value whose type mentions a skolem from a local scope produces errors naming
_$1or similar. Give the type a name with a type parameter and return it through that. - Writing through a wildcard. Updates through
Array[?]ormutable.Buffer[?]fail by design. Route them through a generic helper that names the type. - Migration by deletion. Removing
forSomeand putting?in each position compiles in some cases but quietly breaks a link between positions. Check every rewrite that had one variable in two places.
Trade-offs
Wildcards are cheap and readable, and they are the right tool whenever the code genuinely does not care about the hidden type. Type parameters are the most precise option but push the type onto callers, which is fine for functions and awkward for values stored in collections. Type members carry types with values, at the cost of path-dependent types that some readers find hard to follow. Packed case classes sit in the middle: explicit, pattern-matchable and easy to explain in a code review.
Related techniques solve neighbouring problems. Opaque types hide a representation behind a name rather than hiding an unknown type, and given instances and implicits are how the packed evidence, such as Show[A] above, is found. For the wider picture of variance, bounds and inference, see the Scala type system overview, and for everything else that changed in the language, the Scala 3 guide.
What to do next
- Search your codebase for
forSomeandscala.language.existentials; each hit is a Scala 3 migration task. - Classify every hit: used in one method (type parameter), stored and later used with its evidence (packed case class or type member), or wildcard-expressible (replace with
?). - For each rewrite where one hidden type appeared in two positions, add a test proving the link still holds, such as the
Pairedlist being renderable by its own function. - Adopt
?as the wildcard spelling now; on Scala 2.13 enable-Xsource:3so cross-built code uses one form. - Replace any
Map[Class[?], Any => Unit]-style registry with the packed-handler pattern and bound its type parameter toAnyRef. - When an error message names
_$1or a similar skolem, apply the public-wildcard, private-generic-helper split before trying anything else.