Skip to content

Commit cf84351

Browse files
committed
Add extra amendments from contributor thread and feedback.
1 parent 8888174 commit cf84351

File tree

1 file changed

+81
-9
lines changed

1 file changed

+81
-9
lines changed

content/alternative-bind-variables.md

Lines changed: 81 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ title: SIP-NN - Bind variables within alternative patterns
1313
| Date | Version |
1414
|---------------|--------------------|
1515
| Sep 17th 2023 | Initial Draft |
16+
| Jan 16th 2024 | Amendments |
1617

1718
## Summary
1819

@@ -151,7 +152,7 @@ enum Foo:
151152
~~~
152153

153154
For the expression to make sense with the current semantics around pattern matches, `z` must be defined in both branches; otherwise the
154-
case body would be nonsensical if `z` was referenced within it.
155+
case body would be nonsensical if `z` was referenced within it (see [missing variables](#missing-variables) for a proposed alternative).
155156

156157
Removing the restriction would also allow recursive alternative patterns:
157158

@@ -206,36 +207,107 @@ enum Foo[A]:
206207
case Baz(x) | Bar(x) => // x: Int | A
207208
~~~
208209

210+
### Given bind variables
211+
212+
It is possible to introduce bindings to the contextual scope within a pattern match branch.
213+
214+
Since most bindings will be anonymous but be referred to within the branches, we expect the _types_ present in the contextual scope for each branch to be the same rather than the _names_.
215+
216+
~~~ scala
217+
case class Context()
218+
219+
def run(using ctx: Context): Unit = ???
220+
221+
enum Foo:
222+
case Bar(ctx: Context)
223+
case Baz(i: Int, ctx: Context)
224+
225+
def fun = this match
226+
case Bar(given Context) | Baz(_, given Context) => run // `Context` appears in both branches
227+
~~~
228+
229+
This begs the question of what to do in the case of an explicit `@` binding where the user binds a variable to the same _name_ but to different types. We can either expose a `String | Int` within the contextual scope, or simply reject the code as invalid.
230+
231+
~~~ scala
232+
enum Foo:
233+
case Bar(s: String)
234+
case Baz(i: Int)
235+
236+
def fun = this match
237+
case Bar(x @ given String) | Baz(x @ given Int) => ???
238+
~~~
239+
240+
To be consistent with the named bindings, we argue that the code should compile and a contextual variable added to the scope with the type of `String | Int`.
241+
242+
### Alternatives
243+
244+
#### Enforcing a single type for a bound variable
245+
246+
We could constrain the type for each bound variable within each alternative branch to be the same type. Notably, this is what languages such as Rust, which do not have sub-typing do.
247+
248+
However, since untagged unions are part of Scala 3 and the fact that both are represented by the `|`, it felt more natural to discard this restriction.
249+
250+
#### Type ascriptions in alternative branches
251+
252+
Another suggestion is that an _explicit_ type ascription by a user ought to be defined for all branches. For example, in the currently proposed rules, the following code would infer the return type to be `Int | A` even though the user has written the statement `id: Int`.
253+
254+
~~~scala
255+
enum Foo[A]:
256+
case Bar[A](a: A)
257+
case Baz[A](a: A)
258+
259+
def test = this match
260+
case Bar(id: Int) | Baz(id) => id
261+
~~~
262+
263+
In the author's subjective opinion, it is more natural to view the alternative arms as separate branches — which would be equivalent to the function below.
264+
265+
~~~scala
266+
def test = this match
267+
case Bar(id: Int) => id
268+
case Baz(id) => id
269+
~~~
270+
271+
On the other hand, if it is decided that each bound variable ought to be the same type, then arguably "sharing" explicit type ascriptions across branches would reduce boilerplate.
272+
273+
#### Missing variables
274+
275+
Unlike in other languages, we could assign a type, `A | Null`, to a bind variable which is not present in all of the alternative branches. Rust, for example, is constrained by the fact that the size of a variable must be known and untagged unions do not exist.
276+
277+
Arguably, missing a variable entirely is more likely to be an error — the absence of a requirement for `var` declarations before assigning variables in Python means that beginners can easily assign variables to the wrong variable.
278+
279+
It may be, that the enforcement of having to have the same bind variables within each branch ought to be left to a linter rather thana a hard restriction within the language itself.
280+
209281
## Specification
210282

211283
We do not believe there are any syntax changes since the current specification already allows the proposed syntax.
212284

213285
We propose that the following clauses be added to the specification:
214286

215-
Let $`p_1 | \ldots | p_n`$ be an alternative pattern at an arbitrary depth within a case pattern
216-
and $`\Gamma_n`$ is the scope associated with each alternative.
287+
Let $`p_1 | \ldots | p_n`$ be an alternative pattern at an arbitrary depth within a case pattern and $`\Gamma_n`$ is the named scope associated with each alternative.
217288

218-
Let the variables introduced within each alternative, $`p_n`$, be $`x_i \in \Gamma_n`$.
289+
Let the named variables introduced within each alternative $`p_n`$, be $`x_i \in \Gamma_n`$ and the unnamed contextual variables within each alternative have the type $`T_i \in \Gamma_n`$.
219290

220-
Each $`p_n`$ must introduce the same set of bindings, i.e. for each $`n`$, $`\Gamma_n`$ must have the same members
221-
$`\Gamma_{n+1}`$.
291+
Each $`p_n`$ must introduce the same set of bindings, i.e. for each $`n`$, $`\Gamma_n`$ must have the same **named** members $`\Gamma_{n+1}`$ and the set of $`{T_0, ... T_n}`$ must be the same.
222292

223293
If $`X_{n,i}`$, is the type of the binding $`x_i`$ within an alternative $`p_n`$, then the consequent type, $`X_i`$, of the
224294
variable $`x_i`$ within the pattern scope, $`\Gamma`$ is the least upper-bound of all the types $`X_{n, i}`$ associated with
225295
the variable, $`x_i`$ within each branch.
226296

227297
## Compatibility
228298

229-
We believe the changes are backwards compatible.
299+
We believe the changes would be backwards compatible.
230300

231301
# Related Work
232302

233303
The language feature exists in multiple languages. Of the more popular languages, Rust added the feature in [2021](https://github.com/rust-lang/reference/pull/957) and
234304
Python within [PEP 636](https://peps.python.org/pep-0636/#or-patterns), the pattern matching PEP in 2020. Of course, Python is untyped and Rust does not have sub-typing
235305
but the semantics proposed are similar to this proposal.
236306

237-
Within Scala, the [issue](https://github.com/scala/bug/issues/182) first raised in 2007. The author is also aware of attempts to fix this issue by [Lionel Parreaux](https://github.com/dotty-staging/dotty/compare/main...LPTK:dotty:vars-in-pat-alts) which
238-
were not submitted to the main dotty repository.
307+
Within Scala, the [issue](https://github.com/scala/bug/issues/182) first raised in 2007. The author is also aware of attempts to fix this issue by [Lionel Parreaux](https://github.com/dotty-staging/dotty/compare/main...LPTK:dotty:vars-in-pat-alts) and the associated [feature request](https://github.com/lampepfl/dotty-feature-requests/issues/12) which
308+
was not submitted to the main dotty repository.
309+
310+
The associated [thread](https://contributors.scala-lang.org/t/pre-sip-bind-variables-for-alternative-patterns/6321) has some extra discussion around semantics. Historically, there have been multiple similar suggestions — in [2023](https://contributors.scala-lang.org/t/qol-sound-binding-in-pattern-alternatives/6226) by Quentin Bernet and in [2021](https://contributors.scala-lang.org/t/could-it-be-possible-to-allow-variable-binging-in-patmat-alternatives-for-scala-3-x/5235) by Alexey Shuksto.
239311

240312
## Implementation
241313

0 commit comments

Comments
 (0)