Skip to content

Commit db03567

Browse files
committed
Added more alternatives
1 parent 812de0e commit db03567

File tree

1 file changed

+125
-53
lines changed

1 file changed

+125
-53
lines changed

_sips/sips/2021-06-25-pattern-matching-with-named-fields.md renamed to _sips/sips/2022-02-25-pattern-matching-with-named-fields.md

Lines changed: 125 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,15 @@ redirect_from: /sips/pending/2021-06-25-named-pattern-matching.html
88

99
## History
1010

11-
| Date | Version |
12-
|----------------|---------------|
13-
| June 25th 2015 | Initial Draft |
11+
| Date | Version |
12+
|---------------|---------------|
13+
| Feb 25th 2022 | Initial Draft |
1414

1515
## Motivation
1616

17-
An intuitive, readable, and extendible way to deconstruct case classes in pattern matching.
17+
An readable, extendible, and intuitive way to deconstruct case classes in pattern matching.
1818

19-
// TODO: many times requested, find more references than the contributors thread
20-
21-
// TODO: use lingo of spec
19+
Link to work in progress implementation lives here: https://github.com/Jentsch/dotty
2220

2321
## Motivating Examples
2422

@@ -33,7 +31,7 @@ val annasCity = user match
3331
case User(name = "Anna", city = c) => c
3432
```
3533

36-
The Deconstruction allows the same syntax as the construction and seems to be what people intuitive expect. See //TODO: Examples
34+
The Deconstruction allows the same syntax as the construction and seems to be what people intuitively expect. See for example the very first post in [Scala Contributors Thread][contributors-thread] for this topic.
3735

3836
Without names in patterns user have to use underscore a lot. The example above would be written as, and is the same what the compiler generates:
3937

@@ -44,13 +42,13 @@ val annasCity = user match
4442
case User("Anna", _, c) => c
4543
```
4644

47-
This makes it hard which parameter means what, basically the same idea as for named arguments. (IDEs help here, by showing the names.)
45+
This makes it hard which parameter means what, basically the same idea as for named arguments. Adding underscores until the compile is happy is also not a great experience. IDEs can help here, by showing the names.
4846

49-
In addition, it breaks every time a field of `User` gets added, rearranged, or removed.
50-
In the worst case it breaks silently, if two fields with the type switch places.
47+
In addition, the code breaks every time a field of `User` gets added, rearranged, or removed.
48+
In the worst case it breaks silently, if two fields with the same type switch places.
5149

52-
Personal motivation comes from using to offend https://www.scalatest.org/user_guide/using_matchers#matchingAPattern
53-
and got bitten every time the data model changed slightly.
50+
My personal motivation comes from using [`matchPattern` in ScalaTest](https://www.scalatest.org/user_guide/using_matchers#matchingAPattern)
51+
and got bitten by it every time my data model changed slightly.
5452

5553
## Counter-Examples
5654

@@ -60,31 +58,14 @@ This SIP doesn't aim to allow pattern where parameters go into the pattern, e.g:
6058
val map = Map("Berlin" -> 10, "Paris" -> 5)
6159

6260
map match
63-
case Map("Paris" -> five) => five
61+
case Map("Paris" = five) => five
6462
```
6563

66-
//TODO: find references where this feature was requested
67-
6864
## Design
6965

7066
Goal is similarity between construction and deconstruction of case classes.
7167

72-
Before this was invalid syntax, so this shouldn't affect any existing Scala program.
73-
74-
### Desugaring
75-
76-
> One important principle of Scala’s pattern matching design is that case classes should be abstractable. I.e. we want to be able to represent the abilities of a case class without exposing the case class itself. That also allows code to evolve from a case class to a regular class or a different case class while maintaining the interface of the old case class. [Martin Odersky](https://contributors.scala-lang.org/t/pattern-matching-with-named-fields/1829/52)
77-
78-
To create a user defined destructor user the `@names` annotation.
79-
80-
```scala
81-
class User(val name: String, val age: Int, val city: String)
82-
83-
object User:
84-
@names("name", "age", "city")
85-
def unapply(user: User): Some[(String, Age, String)] =
86-
(user.name, user.age, user.city)
87-
```
68+
Up to now this was invalid syntax, so this shouldn't affect any existing Scala program.
8869

8970
### Mixed usage
9071

@@ -93,30 +74,70 @@ But they have no motivational use case. Maybe they should be disallowed.
9374

9475
```scala
9576
case User("Anna", city = c) => // Mixed usage seems wired
96-
case User(_, city = c) => // Leading underscore are espacially useless
77+
case User(_, city = c) => // Leading underscore are especially useless
9778
```
9879

9980
### Disallow same name twice
10081

101-
// TODO: Motivate this restriction
102-
10382
```scala
10483
case User(city = city1, city = city2) => a // error city is used twice
10584
case User(name1, name = name2) => a // error city is used twice
10685
```
10786

87+
The same should happen if the `User` had a field with a [deprecated name](https://www.scala-lang.org/api/current/scala/deprecatedName.html).
88+
10889
### Order of name and term
10990

110-
Normally an equal sign assigns the result of the right side to the left side. With the proposed syntax that's not the case. However, for most, if not all, people that's the correct. The order seems the requires a look ahead in the parser.
91+
Normally an equal sign assigns the result of the right side to the left side. With the proposed syntax that's not the case. However, for most, if not all, people that's the correct order.
11192

112-
// TODO: Performance test
93+
This order seems the requires a single look ahead in the parser.
11394

114-
## Implementation
95+
### Desugaring
96+
97+
> One important principle of Scala’s pattern matching design is that case classes should be abstractable. I.e. we want to be able to represent the abilities of a case class without exposing the case class itself. That also allows code to evolve from a case class to a regular class or a different case class while maintaining the interface of the old case class. [Martin Odersky](https://contributors.scala-lang.org/t/pattern-matching-with-named-fields/1829/52)
11598
99+
* Allow the same feature of case classes:
100+
* Provide arbitrary names
101+
* Enable [`@deprecatedName`](https://www.scala-lang.org/api/current/scala/deprecatedName.html), which leads to multiple names for a single field. Using both should be an error.
102+
* Maybe provide additional named fields. For example for our `User`:
116103

117-
'Simple' rewrite of patterns. If a pattern with a name is encountered, the compiler looks up the index of those names and places the tree accordingly.
104+
```scala
105+
case User(ageInDays = 3650) => ...
106+
```
107+
108+
But this could be an awful feature.
109+
* Maybe leave fields without names
110+
111+
This SIP proposes basically to use [shapeless records](https://github.com/milessabin/shapeless/wiki/Feature-overview:-shapeless-2.0.0#extensible-records) for the specific purpose of pattern matching.
112+
For incomplete list of alternatives see [alternative desugaring](#alternative-desugaring) below.
113+
114+
Pro:
115+
116+
* with shapeless records we have a good understanding what we are doing, however we need to tweak the encoding a bit
117+
* with some helper definitions the usage would be quite short
118+
* reuse possible
119+
120+
Con:
121+
122+
* opens an can of worms, as this encoding could be useful for other requested features like [named tuples](named-tuple), which would require more thought-out design
123+
* no support for `@deprecatedName`
124+
* identifiers are represented with string literals
125+
126+
Example for user:
127+
128+
```scala
129+
object User:
130+
def unapply(user: User): Option[
131+
(String, Int, String) &
132+
{ type Names = ("name", "age", "city") }
133+
] = (user.name, user.age, user.city).asInstanceOf
134+
```
135+
136+
The reason to pull the names out, instead of keeping them near to their type, is to prevent extractors to accidentally leaking the name.
118137

119-
// TODO: Brag about the simplicity of the implementation
138+
## Implementation
139+
140+
'Simple' rewrite of patterns. If a pattern with a name is encountered, the compiler looks up the index of those names and places the tree accordingly.
120141

121142
Example:
122143

@@ -134,7 +155,6 @@ case User(
134155

135156
## Drawbacks
136157

137-
138158
### Allow skipping arguments
139159

140160
Whenever a single named argument is used in pattern, the pattern can have fewer arguments than the unapply.
@@ -144,42 +164,90 @@ This leads to inconsistency, as pointed out by Lionel Parreaux in the [Scala Con
144164
case User(age = _) => "Just wanted to use the extractor, lol!"
145165
```
146166

147-
148167
## Alternatives
149168

150169
### Without any changes to the language
151170

152171
One alternative way of archiving most objectives, that is doable with current Scala, is to use specialized extractors.
153172

154173
```scala
155-
case class User(age: Int)
156-
157174
object User:
158175
object age:
159176
def unapply(user: User): Option[Int] =
160177
Some(user.age)
161178

162-
User(10) match
179+
user match
163180
case User.age(y) => y
164181
```
165182

166-
Libraries like [Monocle][monocle] could be extended to reduce the boilerplate, but still some boilerplate would remain.
183+
Libraries like [Monocle][monocle] could be extended to reduce the boilerplate, but some boilerplate would remain.
167184
In addition, this breaks the intuitive similarity between construction and deconstruction.
168185

169-
### Type as vehicle
186+
### Alternative desugaring
187+
188+
#### Use underscore methods
189+
190+
In the sprit of https://dotty.epfl.ch/docs/reference/changed-features/pattern-matching.html#name-based-match:
191+
192+
```scala
193+
object User:
194+
class UserMatching(user: User):
195+
def _1 = user.name
196+
...
197+
198+
def _name = user.name
199+
inline def _age = _2
200+
```
201+
202+
Pro:
203+
204+
* allows to add more fields
205+
* allows `@deprecatedName`
206+
207+
Con:
208+
209+
* How to detect that a name means the same as a position? Maybe detect simple patterns like the last line in the example?
210+
* long and verbose, without any shortcuts
170211

171-
But what is with inheritance of the object?
212+
#### Annotated `unapply` method
213+
214+
```scala
215+
object User:
216+
@names("name", "age", "city")
217+
def unapply(user: User): Some[(String, Age, String)] = ...
218+
```
219+
220+
Pro:
221+
222+
* simple to implement (was done in the first draft implementation)
223+
224+
Con:
225+
226+
* no clear way of encoding deprecated name
227+
* annotations are not very scalaish
228+
* no reuse possible
229+
230+
#### Add type as vehicle
172231

173232
```scala
174233
object User:
175234
type Unapply = ("name", "age", "city")
176-
def unapply(user: User): Some((String, Age, String)) =
177-
(user.name, user.age, user.city)
235+
def unapply(user: User): Some((String, Age, String)) = ...
178236
```
179237

238+
Pro:
239+
240+
* should be easy to implement
241+
* the type and the method can inherited from traits and allow some kind of reuse
242+
243+
Con:
244+
245+
* the type and the method can be defined in unrelated traits. Only at the use site of the can be checked if the type and the method agree on the arity of the pattern.
246+
* no clear way of encoding deprecated name
247+
180248
### Named Tuple Arguments / Anonymous Case Classes
181249

182-
This was mentioned in the discussion about [Named Tuple Arguments / Anonymous Case Classes][named-tuple] as bonus, that named tuples could transport the names from unapply to the pattern.
250+
This was mentioned in the discussion about [Named Tuple Arguments / Anonymous Case Classes][named-tuple] as bonus, that named tuples could transport the names from unapply to the pattern.
183251
This would be more generic and could handle user defined extractors.
184252

185253
### Partial destructuring in guards
@@ -191,7 +259,11 @@ Lionel Parreaux proposed a more powerful mechanism, where if guards of cases cou
191259
case User(age = Age(years)) => years // both cases do the same thing
192260
```
193261

194-
His proposal is striclty more powerfull, but arguably less intuitive. Both, Pattern matching with named fields and Partial destructuring in guards could be implemented along each other. Named fields for simple patterns and destructuring in guards for complex patterns. However, they offer two ways to do the same thing and could lead to lots of bike shedding.
262+
His proposal is strictly more powerful, but arguably less intuitive. Both, Pattern matching with named fields and Partial destructuring in guards could be implemented along each other. Named fields for simple patterns and destructuring in guards for complex patterns. However, they offer two ways to do the same thing and could lead to lots of bike shedding, if both got added to the language.
263+
264+
## Open questions
265+
266+
It could be useful to add extractors with just named fields to sealed traits and enums.
195267

196268
## References
197269

@@ -202,6 +274,6 @@ His proposal is striclty more powerfull, but arguably less intuitive. Both, Patt
202274
* [Partial Destructuring in Guards][partial-destructuring-in-guards]
203275

204276
[monocle]: https://www.optics.dev/Monocle/ "Monocle"
205-
[named-tuple]: https://contributors.scala-lang.org/t/named-tuple-arguments-anonymous-case-classes/4352
277+
[named-tuple]: https://contributors.scala-lang.org/t/named-tuple-arguments-anonymous-case-classes/4352
206278
[contributors-thread]: https://contributors.scala-lang.org/t/pattern-matching-with-named-fields/1829/20 "Scala Contributors thread"
207279
[partial-destructuring-in-guards]: http://lptk.github.io/programming/2018/12/12/scala-pattern-warts-improvements.html#-partial-destructuring-in-guards

0 commit comments

Comments
 (0)