Skip to content

Commit 3148b6f

Browse files
committed
Add first draft for named pattern matching
1 parent 013494b commit 3148b6f

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
---
2+
layout: sip
3+
title: SIP-NN - Pattern matching with named fields
4+
vote-status: pending
5+
permalink: /sips/:title.html
6+
redirect_from: /sips/pending/2021-06-25-named-pattern-matching.html
7+
---
8+
9+
## History
10+
11+
| Date | Version |
12+
|----------------|---------------|
13+
| June 25th 2015 | Initial Draft |
14+
15+
## Motivation
16+
17+
An intuitive, readable, and extendible way to deconstruct case classes in pattern matching.
18+
19+
## Motivating Examples
20+
21+
Given one wants to use pattern matching on a case class:
22+
23+
```scala
24+
case class User(name: String, age: Int, city: String)
25+
26+
val user = User(name = "Anna", age = 10, city = "Berlin")
27+
28+
val annasCity = user match
29+
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")) => ???
33+
```
34+
35+
The Deconstruction allows the same syntax as the construction and seems to be what people intuitive expect. See //TODO: Examples
36+
37+
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:
38+
39+
```scala
40+
val annasCity = user match
41+
case User("Anna", _, c) => c
42+
// wild stuff:
43+
case User(s"To$_", _, city) => ???
44+
case User(guy @ ("Guy" | "guy"), _, _) => ???
45+
```
46+
47+
This makes it hard which parameter means what, basically the same idea as for named arguments. (The IDE can help here)
48+
49+
In addition, it breaks every time a field of `User` gets added, rearranged, or removed. In the worst case it breaks silently, if to fields with the type switch places.
50+
51+
Personal motivation comes from using to offend https://www.scalatest.org/user_guide/using_matchers#matchingAPattern
52+
and got bitten every time the data model changed slightly.
53+
54+
## Counter-Examples
55+
56+
This SIP doesn't aim to allow pattern where parameters go into the pattern, e.g:
57+
58+
```scala
59+
val map = Map("Berlin" -> 10, "Paris" -> 5)
60+
61+
map match {
62+
case Map("Paris" -> five) => five
63+
}
64+
65+
4 match {
66+
case n / 2 => "douple of " + n.toString
67+
case _ => "odd"
68+
}
69+
```
70+
71+
//TODO: find references where is feature was requested
72+
73+
## Design
74+
75+
Goal is similarity between construction and deconstruction of case classes.
76+
77+
Before this was invalid syntax, so this shouldn't affect any existing Scala program.
78+
79+
### Open questions
80+
81+
Various patterns are allowed to keep the similarity, but have no motivational use case. Maybe those should be allowed:
82+
83+
```scala
84+
case User("Anna", city = c) => // Mixed usage seems wired
85+
case User(_, city = c) => // Leading underscore are espacially to useless (?)
86+
```
87+
88+
Discuss design decisions (including, as examples):
89+
90+
* What's with user defined `unapply` on case classes? (Design)
91+
92+
## Implementation
93+
94+
95+
'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.
96+
97+
Example:
98+
99+
```scala
100+
// a match clause like
101+
case User(age = <tree>) => ???
102+
103+
// gets rewritten to:
104+
case User(
105+
_, // because name isn't mentioned
106+
<tree>, // because age is the second parameter of user
107+
_ // because city isn't mentioned
108+
)
109+
```
110+
111+
112+
## Drawbacks
113+
114+
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.
115+
116+
```scala
117+
case class Age(years: Int)
118+
119+
class Age(val years: Int) {
120+
// equals, hashcode etc.
121+
}
122+
object Age {
123+
def unapply(age: Age): Option[Int] =
124+
Some(age.years)
125+
}
126+
```
127+
128+
## Alternatives
129+
130+
### Without any changes to the language
131+
132+
One alternative way of archiving most objectives, that is doable with current Scala, is to use specialized extractors.
133+
134+
```scala
135+
case class User(age: Int)
136+
137+
object User {
138+
object age {
139+
def unapply(user: User): Option[Int] =
140+
Some(user.age)
141+
}
142+
}
143+
144+
User(10) match {
145+
case User.age(y) => y
146+
}
147+
```
148+
149+
Libraries like [Monocle][2] could be extended to reduce the boilerplate, but still some boilerplate would remain.
150+
In addition, this breaks the intuitive similarity between construction and deconstruction.
151+
152+
### Records, Tuples with names etc.
153+
154+
//TODO
155+
156+
Would be more generic, could be handle user defined extractors, also could lead naturally to a way to hand;e Counter-Example above.
157+
158+
### Partial destructuring
159+
160+
Lionel Parreaux proposed a more powerful mechanism:
161+
http://lptk.github.io/programming/2018/12/12/scala-pattern-warts-improvements.html#-partial-destructuring-in-guards
162+
163+
If this SIP gets accepted, it could restrict the design any of the last two alternatives, if they come into being.
164+
165+
[2]: https://www.optics.dev/Monocle/ "Monocle"
166+
[4]: https://github.com/dogescript/dogescript "Alternatives"
167+
[5]: https://contributors.scala-lang.org/t/pattern-matching-with-named-fields/1829/20 "Scala Contributors thread"

0 commit comments

Comments
 (0)