Pre-SIP Inline Refinement Captures in Match Type Patterns

Summary

This Pre-SIP proposes allowing match type case patterns to capture abstract type members of refined types, without a separate type alias.

Currently:

type BoxOf[c] = Box { type Content = c }

type Unbox[B <: Box] = B match
  case BoxOf[c] => c

After:

type Unbox[B <: Box] = B match
  case Box { type Content = c } => c

Motivation

Match types already support capturing type arguments via var-patterns — bare lowercase identifiers in type-argument position:

type Elem[X] = X match
  case List[a] => a // `a` is a pattern-bound type variable

However, capturing abstract type members requires an intermediate type alias, which is a pure syntactic tax: BoxOf[c] carries no information beyond what Box { type Content = c }
already expresses. The workaround has several drawbacks:

  • It pollutes the namespace of the enclosing type or object.
  • In a library, it leaks an implementation detail into the public API.
  • It requires the reader to jump to a separate definition to understand the pattern.

Proposed Solution

Allow a bare lowercase identifier on the right-hand side of a type-member refinement inside a match type case pattern to act as a pattern-bound type variable — the same scoping rules as var-patterns in type-argument position.

User-visible syntax

case ParentType { type MemberName = varIdent } => Body

varIdent must be:

  • a bare lowercase identifier (same rule as var-patterns everywhere in match types), and
  • not already in scope as a named type.

It is then bound as a fresh pattern-bound type variable for the duration of Body, with bounds inherited from the corresponding member of ParentType.

Examples

Basic extraction:

trait Box:
  type Content

type Unbox[B <: Box] = B match
  case Box { type Content = c } => c

class StringBox extends Box:
  type Content = String

val _: Unbox[StringBox] = "hello" // OK

Multiple captures in one pattern:

trait KV:
  type Key
  type Value

type PairOf[K <: KV] = K match
  case KV { type Key = k; type Value = v } => (k, v)

Mixed captured and fixed refinements:

type MT[x <: SomeTrait] = x match
  case SomeTrait { type Concrete = Int; type Abstract = a } => a

I will submit a PR if this proposal receives the green light. The implementation seems fairly straightforward: likely around 15 lines in Typer.scala . However, since I am far from being a compiler expert, I might be underestimating the effort. I will link a draft PR with the implementation soon.

4 Likes

Draft implementation Pre-SIP Desugar inline refinement type captures in match type patterns. by halotukozak · Pull Request #26381 · scala/scala3 · GitHub

If it can be made to work, IMO this is worth it.

4 Likes

SIP PR: SIP-NN Inline Refinement Captures in Match Type Patterns by halotukozak · Pull Request #137 · scala/improvement-proposals · GitHub

When first working with type parameters and type members i was surprised they collide without the ability to “upgrade” a type parameter to type member. That would reduce the need for intermediate names. Personally, i like to give type parameters more meaningful names than ‘A’ or ‘B’. So, instead of the above:

class Box[c] :
  type Content = c

we can than simply write

class Box[Content] :
  type Content

would that not also make the problem addressed in this post simpler? Since this is at the moment a compiler error, it would probably not raise new problems.

You can check this paper, something like that was mentioned there

class Box[type Content]

but

Nevertheless, it’s not a part of this SIP.

1 Like