Skip to content

Commit 57e4326

Browse files
committed
Add desugaring
1 parent 0d8a2e4 commit 57e4326

File tree

1 file changed

+63
-19
lines changed

1 file changed

+63
-19
lines changed

_sips/sips/2021-06-25-pattern-matching-with-named-fields.md

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ redirect_from: /sips/pending/2021-06-25-named-pattern-matching.html
1616

1717
An intuitive, readable, and extendible 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
22+
1923
## Motivating Examples
2024

2125
Given one wants to use pattern matching on a case class:
@@ -27,9 +31,6 @@ val user = User(name = "Anna", age = 10, city = "Berlin")
2731

2832
val annasCity = user match
2933
case User(name = "Anna", city = c) => c
30-
// wild stuff:
31-
case User(city = city, name = s"To$_") => ???
32-
case User(name = guy @ ("Guy" | "guy")) => ???
3334
```
3435

3536
The Deconstruction allows the same syntax as the construction and seems to be what people intuitive expect. See //TODO: Examples
@@ -41,12 +42,9 @@ Without names in patterns user have to use underscore a lot. The example above w
4142
```scala
4243
val annasCity = user match
4344
case User("Anna", _, c) => c
44-
// wild stuff:
45-
case User(s"To$_", _, city) => ???
46-
case User(guy @ ("Guy" | "guy"), _, _) => ???
4745
```
4846

49-
This makes it hard which parameter means what, basically the same idea as for named arguments. (The IDE can help here)
47+
This makes it hard which parameter means what, basically the same idea as for named arguments. (IDEs help here, by showing the names.)
5048

5149
In addition, it breaks every time a field of `User` gets added, rearranged, or removed.
5250
In the worst case it breaks silently, if two fields with the type switch places.
@@ -73,6 +71,21 @@ Goal is similarity between construction and deconstruction of case classes.
7371

7472
Before this was invalid syntax, so this shouldn't affect any existing Scala program.
7573

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+
```
88+
7689
### Mixed usage
7790

7891
Mixed patterns, with positional and named patterns are allowed to keep the similarity.
@@ -83,11 +96,28 @@ But they have no motivational use case. Maybe they should be disallowed.
8396
case User(_, city = c) => // Leading underscore are espacially useless
8497
```
8598

99+
### Disallow same name twice
100+
101+
// TODO: Motivate this restriction
102+
103+
```scala
104+
case User(city = city1, city = city2) => a // error city is used twice
105+
case User(name1, name = name2) => a // error city is used twice
106+
```
107+
108+
### Order of name and term
109+
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.
111+
112+
// TODO: Performance test
113+
86114
## Implementation
87115

88116

89117
'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.
90118

119+
// TODO: Brag about the simplicity of the implementation
120+
91121
Example:
92122

93123
```scala
@@ -104,18 +134,16 @@ case User(
104134

105135
## Drawbacks
106136

107-
Without allowing user defined named arguments in pattern matching, the fact that class is a case class becomes part if it's public interface. Changing a case class to a normal class is a backward incompatible change, that library maintainers of to be aware. This is especially worrying since currently libraries where designed without this feature in mind.
108137

109-
```scala
110-
class User(name: String, age: Int, city: String)
111-
object User
112-
def unapply(user: User): Option[(String, Int, String)] =
113-
???
138+
### Allow skipping arguments
114139

115-
// matching on User doesn't work anymore
140+
Whenever a single named argument is used in pattern, the pattern can have fewer arguments than the unapply.
141+
This leads to inconsistency, as pointed out by Lionel Parreaux in the [Scala Contributors Thread](https://contributors.scala-lang.org/t/pattern-matching-with-named-fields/1829/44). This could lead users to use a named pattern, just to skip all parameters.
142+
143+
```scala
144+
case User(age = _) => "Just wanted to use the extractor, lol!"
116145
```
117146

118-
This limitation could be overcome by Named Tuple Arguments, discussed below.
119147

120148
## Alternatives
121149

@@ -138,26 +166,42 @@ User(10) match
138166
Libraries like [Monocle][monocle] could be extended to reduce the boilerplate, but still some boilerplate would remain.
139167
In addition, this breaks the intuitive similarity between construction and deconstruction.
140168

169+
### Type as vehicle
170+
171+
But what is with inheritance of the object?
172+
173+
```scala
174+
object User:
175+
type Unapply = ("name", "age", "city")
176+
def unapply(user: User): Some((String, Age, String)) =
177+
(user.name, user.age, user.city)
178+
```
179+
141180
### Named Tuple Arguments / Anonymous Case Classes
142181

143182
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.
144183
This would be more generic and could handle user defined extractors.
145184

146-
However this isn't much of an alternative, but more of a generalization.
147-
148185
### Partial destructuring in guards
149186

150-
Lionel Parreaux proposed a more powerful mechanism:
151-
http://lptk.github.io/programming/2018/12/12/scala-pattern-warts-improvements.html#-partial-destructuring-in-guards
187+
Lionel Parreaux proposed a more powerful mechanism, where if guards of cases could them self contains destructuring patterns.
188+
189+
```scala
190+
case user: User if Age(years) <- user => years
191+
case User(age = Age(years)) => years // both cases do the same thing
192+
```
152193

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.
153195

154196
## References
155197

156198
* [Scala Contributors Thread][contributors-thread]
157199

158200
* [Monocle][monocle]
159201
* [Named Tuple Arguments / Anonymous Case Classes][named-tuple]
202+
* [Partial Destructuring in Guards][partial-destructuring-in-guards]
160203

161204
[monocle]: https://www.optics.dev/Monocle/ "Monocle"
162205
[named-tuple]: https://contributors.scala-lang.org/t/named-tuple-arguments-anonymous-case-classes/4352
163206
[contributors-thread]: https://contributors.scala-lang.org/t/pattern-matching-with-named-fields/1829/20 "Scala Contributors thread"
207+
[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)