You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: content/match-types-spec.md
+62-21Lines changed: 62 additions & 21 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,10 +30,12 @@ Illegal match types are rejected, which is a breaking change, and can be recover
30
30
## Motivation
31
31
32
32
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:
34
36
35
37
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'`,
37
39
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`.
38
40
39
41
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
42
44
When we read TASTy files, we do not have to perform the work of type inference again; we reuse what was already computed.
43
45
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.
44
46
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
+
46
51
In order to guarantee compatibility, we must ensure that, for any given match type:
47
52
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).
50
61
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.
52
65
53
66
In order to solve this problem, this SIP provides a specification for match type reduction that is independent of the `TypeComparer` black box.
54
67
It defines a subset of match type cases that are considered legal.
55
68
Legal cases get a specification for when and how they should reduce for any scrutinee.
56
69
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.
58
70
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.
61
80
62
81
## Proposed solution
63
82
64
-
### High-level overview
83
+
### Specification
84
+
85
+
#### Preamble
65
86
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.
67
90
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*).
69
96
70
97
#### Syntax
71
98
@@ -75,19 +102,31 @@ The way that a pattern is parsed and type captures identified is kept as is.
75
102
Once type captures are identified, we can represent the *abstract* syntax of a pattern as follows:
76
103
77
104
```
105
+
// Top-level pattern
78
106
MatchTypePattern ::= TypeWithoutCapture
79
107
| MatchTypeAppliedPattern
80
108
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]`
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
+
91
130
The cases `MatchTypeAppliedPattern` are only chosen if they contain at least one `TypeCapture`.
92
131
Otherwise, they are considered `TypeWithoutCapture` instead.
93
132
Each named capture appears exactly once.
@@ -134,7 +173,7 @@ type ZExtractor[t] = Base { type Z = t }
134
173
typeIsSeq[t <:Seq[Any]] = t
135
174
```
136
175
137
-
Here are example of legal patterns:
176
+
Here are examples of legal patterns:
138
177
139
178
```scala
140
179
// TypeWithoutCapture's
@@ -255,7 +294,7 @@ In exchange, it promises that all the patterns that are considered legal will ke
255
294
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.
256
295
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.
257
296
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.
259
298
These 49 libraries contained a total of 779 match type `case`s.
260
299
Of those, there were 8 `case`s that would be flagged as not legal by the current proposal.
261
300
@@ -327,23 +366,25 @@ At the beginning, we were hoping that we could restrict match cases to class typ
327
366
The quantitative study however revealed that we had to introduce support for abstract type constructors and for type member extractors.
328
367
329
368
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.
331
372
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.
334
375
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.
335
376
336
377
Type member extractors are our biggest pain point.
337
378
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).
339
380
In each case, they seem to be a way to express what Scala 2 type projections (`A#T`) could express.
340
381
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.
341
382
342
383
As far as we know, those use cases have no workaround if we make type member extractors illegal.
343
384
344
385
## Related work
345
386
346
-
This section should list prior work related to the proposal, notably:
387
+
Notable prior work related to this proposal includes:
347
388
348
389
-[Current reference page for Scala 3 match types](https://dotty.epfl.ch/docs/reference/new-types/match-types.html)
349
390
-["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