Skip to content

Commit cb6b892

Browse files
committed
Address most of Seth's comments.
1 parent 4ae0ee0 commit cb6b892

File tree

1 file changed

+62
-21
lines changed

1 file changed

+62
-21
lines changed

content/match-types-spec.md

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,12 @@ Illegal match types are rejected, which is a breaking change, and can be recover
3030
## Motivation
3131

3232
Currently, match type reduction is implementation-defined.
33-
Matching a scrutinee type `X` against a pattern `P` with captures `ts` works as follows:
33+
The core of the logic is matching a scrutinee type `X` against a pattern `P` with captures `ts`.
34+
Captures are similar to captures in term-level pattern matching: in the type-level `case List[t] =>`, `t` is a (type) capture, just like in the term-level `case List(x) =>`, `x` is a (term) capture.
35+
Matching works as follows:
3436

3537
1. we create new type variables for the captures `ts'` giving a pattern `P'`,
36-
2. we ask the compiler's `TypeComparer` (the type inference black blox) to "try and make it so" that `X <:< P'`,
38+
2. we ask the compiler's `TypeComparer` (the type inference black box) to "try and make it so" that `X <:< P'`,
3739
3. if it manages to do so, we get constraints for `ts'`; we then have a match, and we instantiate the body of the pattern with the received constraints for `ts`.
3840

3941
The problem with this approach is that, by essence, type inference is an unspecified black box.
@@ -42,30 +44,55 @@ This is fine everywhere else in the language, because what type inference comes
4244
When we read TASTy files, we do not have to perform the work of type inference again; we reuse what was already computed.
4345
When a new version of the compiler changes type inference, it does not change what was computed and stored in TASTy files by previous versions of the compiler.
4446

45-
For match types, this is a problem, because reduction spans across TASTy file.
47+
For match types, this is a problem, because reduction spans across TASTy files.
48+
Given some match type `type M[X] = X match { ... }`, two codebases may refer to `M[SomeType]`, and they must reduce it in the same way.
49+
If they don't, hard incompatibilities can appear, such as broken subtyping, broken overriding relationships, or `AbstractMethodError`s due to inconsistent erasure.
50+
4651
In order to guarantee compatibility, we must ensure that, for any given match type:
4752

48-
* if it reduces in a given way in verion 1 of the compiler, it still reduces in the same way in version 2, and
49-
* if it decides disjointness in version 1, it still decides disjointness in version 2.
53+
* if it reduces in a given way in version 1 of the compiler, it still reduces in the same way in version 2, and
54+
* if it does not reduce in version 1 of the compiler, it still does not reduce in version 2.
55+
* (it is possible for version 1 to produce an *error* while version 2 successfully reduces or does not reduce)
56+
57+
Reduction depends on two decision produces:
58+
59+
* *matching* a scrutinee `X` against a pattern `P` (which we mentioned above), and
60+
* deciding that a scrutinee `X` is *provably disjoint* from a pattern `P` (disjointness is not affected by this SIP; see below).
5061

51-
By delegating reduction to the `TypeComparer` black box, it is in practice impossible to guarantee the former.
62+
When a scrutinee does not match a given pattern and cannot be proven disjoint from it either, the match type is "stuck" and does not reduce.
63+
64+
If matching is delegated to the `TypeComparer` black box, then it is impossible in practice to guarantee the first compatibility property.
5265

5366
In order to solve this problem, this SIP provides a specification for match type reduction that is independent of the `TypeComparer` black box.
5467
It defines a subset of match type cases that are considered legal.
5568
Legal cases get a specification for when and how they should reduce for any scrutinee.
5669
Illegal cases are rejected as being outside of the language.
57-
For compatibility reasons, they can still be accepted with `-source:3.3`; in that case, they reduce using the existing, unspecified (and prone to breakage) implementation.
5870

59-
For legal cases, the proposed reduction specification should reduce in the same way as the current implementation for the majority of cases.
60-
That is however not possible to guarantee, since the existing implementation is not specified in the first place.
71+
For compatibility reasons, such now-illegal cases will still be accepted under `-source:3.3`; in that case, they reduce using the existing, unspecified (and prone to breakage) implementation.
72+
Due to practical reasons (see "Other concerns"), doing so does *not* emit any warning.
73+
Eventually, support for this fallback may be removed if the compiler team decides that its maintenance burden is too high.
74+
As usual, this SIP does not by itself provide any specific timeline.
75+
In particular, there is no relationship with 3.3 being an "LTS"; it just happens to be the latest "Next" as well at the time of this writing (the changes in this SIP will only ever apply to 3.4 onwards, so the LTS is not affected in any way).
76+
77+
For legal cases, the proposed reduction specification should reduce in the same way as the current implementation for all but the most obscure cases.
78+
Our tests, including the entire dotty CI and its community build, did not surface any such incompatibility.
79+
It is however not possible to guarantee that property for *all* cases, since the existing implementation is not specified in the first place.
6180

6281
## Proposed solution
6382

64-
### High-level overview
83+
### Specification
84+
85+
#### Preamble
6586

66-
By its nature, this proposal only contains a specification, without any high level overview.
87+
Some of the concepts mentioned here are defined in the existing Scala 3 specification draft.
88+
That draft can be found in the dotty repository at https://github.com/lampepfl/dotty/tree/main/docs/_spec.
89+
It is not rendered anywhere yet, though.
6790

68-
### Specification
91+
Here are some of the relevant concepts that are perhaps lesser-known:
92+
93+
* Chapter 3, section "Internal types": concrete v abstract syntax of types.
94+
* Chapter 3, section "Base Type": the `baseType` function.
95+
* Chapter 3, section "Definitions": whether a type is concrete or abstract (unrelated to the concrete or abstract *syntax*).
6996

7097
#### Syntax
7198

@@ -75,19 +102,31 @@ The way that a pattern is parsed and type captures identified is kept as is.
75102
Once type captures are identified, we can represent the *abstract* syntax of a pattern as follows:
76103

77104
```
105+
// Top-level pattern
78106
MatchTypePattern ::= TypeWithoutCapture
79107
| MatchTypeAppliedPattern
80108
109+
// A type that does not contain any capture, such as `Int` or `List[String]`
110+
TypeWithoutCapture ::= Type // `Type` is from the "Internal types" section of the spec
111+
112+
// Applied type pattern with at least one capture, such as `List[Seq[t]]` or `*:[Int, t]`
81113
MatchTypeAppliedPattern ::= TyconWithoutCapture ‘[‘ MatchTypeSubPattern { ‘,‘ MatchTypeSubPattern } ‘]‘
82114
115+
// The type constructor of a MatchTypeAppliedPattern never contains any captures
116+
TyconWithoutCapture ::= Type
117+
118+
// Type arguments can be captures, types without captures, or nested applied patterns
83119
MatchTypeSubPattern ::= TypeCapture
84120
| TypeWithoutCapture
85121
| MatchTypeAppliedPattern
86122
87-
TypeCapture ::= NamedTypeCapture
88-
| WildcardTypeCapture
123+
TypeCapture ::= NamedTypeCapture // e.g., `t`
124+
| WildcardTypeCapture // `_` (with inferred bounds)
89125
```
90126

127+
In the concrete syntax, `MatchTypeAppliedPattern`s can take the form of `InfixType`s.
128+
A common example is `case h *: t =>`, which is desugared into `case *:[h, t] =>`.
129+
91130
The cases `MatchTypeAppliedPattern` are only chosen if they contain at least one `TypeCapture`.
92131
Otherwise, they are considered `TypeWithoutCapture` instead.
93132
Each named capture appears exactly once.
@@ -134,7 +173,7 @@ type ZExtractor[t] = Base { type Z = t }
134173
type IsSeq[t <: Seq[Any]] = t
135174
```
136175

137-
Here are example of legal patterns:
176+
Here are examples of legal patterns:
138177

139178
```scala
140179
// TypeWithoutCapture's
@@ -255,7 +294,7 @@ In exchange, it promises that all the patterns that are considered legal will ke
255294
In order to evaluate the practical impact of this proposal, we conducted a quantitative analysis of *all* the match types found in Scala 3 libraries published on Maven Central.
256295
We used [Scaladex](https://index.scala-lang.org/) to list all Scala 3 libraries, [coursier](https://get-coursier.io/docs/api) to resolve their classpaths, and [tasty-query](https://github.com/scalacenter/tasty-query) to semantically analyze the patterns of all the match types they contain.
257296

258-
Out of 4,783 libraries that were found and analyzed, 49 contained at least one match type definition.
297+
Out of 4,783 libraries, 49 contained at least one match type definition.
259298
These 49 libraries contained a total of 779 match type `case`s.
260299
Of those, there were 8 `case`s that would be flagged as not legal by the current proposal.
261300

@@ -327,23 +366,25 @@ At the beginning, we were hoping that we could restrict match cases to class typ
327366
The quantitative study however revealed that we had to introduce support for abstract type constructors and for type member extractors.
328367

329368
As already mentioned, the standard library itself contains an occurrence of an abstract type constructor in a pattern.
330-
Making that an error would mean declaring the standard library itself bankrupt, which was not a viable option.
369+
If we made that an error, we would have a breaking change to the standard library itself.
370+
Some existing libraries would not be able to retypecheck again.
371+
Worse, it might not be possible for them to change their code in a way that preserves their own public APIs.
331372

332-
We tried to restrict abstract type constructor to never match on their own.
333-
Instead, we wanted them to stay *stuck* until they could be instantiated to a concrete type constructor.
373+
We tried to restrict abstract type constructors to never match on their own.
374+
Instead, we wanted them to stay "stuck" until they could be instantiated to a concrete type constructor.
334375
However, that led some existing tests to fail even for match types that were declared legal, because they did not reduce anymore in some places where they reduced before.
335376

336377
Type member extractors are our biggest pain point.
337378
Their specification is complicated, and the implementation as well.
338-
Our quantitative study showed that they were however "often" used (10 occurrences spread over 4 libraries).
379+
Our quantitative study showed that they were however used at least somewhat often (10 occurrences spread over 4 libraries).
339380
In each case, they seem to be a way to express what Scala 2 type projections (`A#T`) could express.
340381
While not quite as powerful as type projections (which were shown to be unsound), match types with type member extractors delay things enough for actual use cases to be meaningful.
341382

342383
As far as we know, those use cases have no workaround if we make type member extractors illegal.
343384

344385
## Related work
345386

346-
This section should list prior work related to the proposal, notably:
387+
Notable prior work related to this proposal includes:
347388

348389
- [Current reference page for Scala 3 match types](https://dotty.epfl.ch/docs/reference/new-types/match-types.html)
349390
- ["Pre-Sip" discussion in the Contributors forum](https://contributors.scala-lang.org/t/pre-sip-proper-specification-for-match-types/6265) (submitted at the same time as this SIP document)

0 commit comments

Comments
 (0)