Scala 3: Safer Pattern Matching with Matchable

Dean Wampler
Scala 3
Published in
5 min readApr 28, 2021

--

Late in the Scala 3 development process, a new type scala.Matchable was introduced to fix a loophole in pattern matching. This post discusses that loophole, how Matchable fixes it, and implications of this change.

Lincoln Park Education Pavilion Detail, © 2021, Dean Wampler

For an even more concise summary of most of the notable changes in Scala 3, see my new Scala 3 Highlights page.

Like Scala 3 itself, Programming Scala, Third Edition is almost finished! You can read a draft of it on the O’Reilly Learning Platform.

Arrays in Scala have always been Java arrays, which are mutable. Scala 3 introduces a new immutable wrapper around arrays call IArray, which disallows modification, but pattern matching caused a hole:

Running this code in the Scala 3 REPL returns this output for a supposedly immutable array wrapper:

val iarray: opaques.IArray[Int] = Array(1, 2, 300, 4, 5)

However, if you invoke the compiler or REPL with the flag -source:future (which I recommend, once you’ve transitioned your code base from Scala 2), you get a warning, too:

val iarray: IArray[Int] = Array(1, 2, 300, 4, 5)
3 | case a: Array[Int] => a(2) = 300 // Scala 3 warning!!
| ^^^^^^^^^^
| pattern selector should be an instance of Matchable,
| but it has unmatchable type IArray[Int] instead

scala.Matchable is a new universal trait introduced into Scala’s type hierarchy that is designed to be a parent type of all allowed pattern matching expressions. Matchable's parent is Any, so it is a new sibling of AnyVal and AnyRef, the first one introduced since the early days of Scala! Both AnyVal and AnyRef are subtypes of Matchable, so concrete values and reference types will continue to work in pattern matching expressions without change.

However, this is is a breaking change, as I’ll demonstrate below, which is why the -source:future flag is required in Scala 3.0 to trigger the warning about the need for Matchable.

So, how does Matchable help in this situation involving IArray? Recall in Scala 3: Opaque Type Aliases and Open Classes, I discussed the new opaque type aliases feature. IArray is declared as an opaque type alias:

opaque type IArray[+T] = Array[_ <: T]

IArray, like all opaque types, is considered an abstract type by the compiler, not concrete, and it is not explicitly declared as a subtype of Matchable, which is why we get the warning when -source:future is used.

By the way, I mentioned that Matchable is a universal trait, which are traits with the following properties:

  • They subtype Any, but not any other universal traits.
  • They define only methods.
  • They do no initialization of their own.

Universal traits are important for defining value classes, which were also discussed in my post on opaque type aliases. (Both universal traits and value classes are Scala 2 features.)

Consequences of Matchable

While pattern matching for concrete values and reference types will continue work without change, the Dotty page for Matchable says that a pattern-matching clause will trigger a warning for the following cases:

  • Type Any: if pattern matching is required one should use Matchable instead.
  • Unbounded type parameters and abstract types: If pattern matching is required they should have an upper bound Matchable.
  • Type parameters and abstract types that are only bounded by some universal trait: Again, Matchable should be added as a bound.

The second and third bullets will impact a lot of code you write. Consider the following two implementations of an examine method:

With -source:future, examine1 will throw this warning:

2 |  case i: Int => s"Int: $i"
| ^^^
| pattern selector should be an instance of Matchable,
| but it has unmatchable type T instead

Previously, this was perfectly acceptable Scala code. The correct implementation now uses the type bound shown for examine2. Hence, in a future release of Scala 3 that makes this warning the default behavior, you’ll need to modify all such pattern matching expressions that involve abstract types or type parameters, like this example.

You’ll also encounter situations where Matchable will now be inferred as the least upper bound, instead of Any, which would have been inferred previously:

In Scala 2, seq would have type Seq[Any]. Similarly, in the last case clause shown, case unexpected, the type of unexpected will be inferred to be Matchable, not Any, as it would be in Scala 2.

Finally, Scala 3's support for dependently-typed methods is impacted by this change. Recall that I discussed dependent typing in two posts, part 1 and part 2, but I didn’t discuss dependently-typed methods in those posts.

Here is an example of a dependent type declaration and a dependently-typed method that uses it:

First, ElemR is a recursive match type alias that returns the actual type used for the type parameter of a higher-kinded type’s. For example, for Seq[Foo], it will return type Foo. See the Appendix in the dependent typing part 2 post for more details on match types.

ElemR is used by first, a dependently-typed method that returns the first element in a “container” or just the item itself for other instances. Note the return type ElemR means the actual type of the instance returned will be used, not Any or similar. In other words, the method’s return type is dependent on the instance, which is how dependently-typed methods get their name. The examples at the end show what happens. Note the specific types shown for returned values.

If you replace x.asMatchable with x, you’ll get the same type warnings we saw previously, even though the definition of ElemR doesn’t trigger this complaint itself.

The alternative to using asMatchable is to change the type parameter for first to be[X <: Matchable]. Then you can use x match ... in the body without problems.

For more on Matchable, see the Dotty documentation for it, which also discusses the implications for equality checking and how some Any methods might be moved to Matchable in a future Scala release.

For an even more concise summary of most of the notable changes in Scala 3, see my new Scala 3 Highlights page.

Like Scala 3 itself, Programming Scala, Third Edition is almost finished! You can read a draft of it on the O’Reilly Learning Platform.

--

--

Dean Wampler
Scala 3

The person who is wrong on the Internet. ML/AI and FP enthusiast. Lurks at the AI Alliance and IBM Research. Speaker, author, pretend photographer.