Skip to content

Commit 633fe1f

Browse files
committed
Add conversion from named to unnamed tuples
1 parent 8c782c2 commit 633fe1f

File tree

1 file changed

+46
-13
lines changed

1 file changed

+46
-13
lines changed

content/named-tuples.md

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -65,29 +65,46 @@ Example:
6565
println(x)
6666
~~~
6767

68-
### Conformance
68+
### Conformance and Convertibility
6969

7070
The order of names in a named tuple matters. For instance, the type `Person` above and the type `(age: Int, name: String)` would be different, incompatible types.
7171

7272
Values of named tuple types can also be be defined using regular tuples. For instance:
7373
```scala
74-
val x: Person = ("Laura", 25)
74+
val Laura: Person = ("Laura", 25)
7575

7676
def register(person: Person) = ...
7777
register(person = ("Silvain", 16))
7878
register(("Silvain", 16))
7979
```
80-
This follows since a regular tuple `(T_1, ..., T_n)` is treated as a subtype of a named tuple `(N_1 = T_1, ..., N_n = T_n)` with the same element types. On the other hand, named tuples do not conform to unnamed tuples, so the following is an error:
81-
```scala
82-
val x: (String, Int) = Bob // error: type mismatch
83-
```
84-
One can convert a named tuple to an unnamed tuple with the `toTuple` method, so the following works:
80+
This follows since a regular tuple `(T_1, ..., T_n)` is treated as a subtype of a named tuple `(N_1 = T_1, ..., N_n = T_n)` with the same element types.
81+
82+
In the other direction, one can convert a named tuple to an unnamed tuple with the `toTuple` method. Example:
8583
```scala
8684
val x: (String, Int) = Bob.toTuple // ok
8785
```
86+
`toTuple` is defined as an extension method in the `NamedTuple` object.
87+
It returns the given tuple unchanged and simply "forgets" the names.
8888

89-
_Question:_ Should we define an implicit conversion, either in place of this method or in addition to it?
90-
89+
A `.toTuple` selection is inserted implicitly by the compiler if it encounters a named tuple but the expected type is a regular tuple. So the following works as well:
90+
```scala
91+
val x: (String, Int) = Bob // works, expanded to Bob.toTuple
92+
```
93+
The difference between subtyping in one direction and automatic `.toTuple` conversions in the other is relatively minor. The main difference is that `.toTuple` conversions don't work inside type constructors. So the following is OK:
94+
```scala
95+
val names = List("Laura", "Silvain")
96+
val ages = List(25, 16)
97+
val persons: List[Person] = names.zip(ages)
98+
```
99+
But the following would be illegal.
100+
```scala
101+
val persons: List[Person] = List(Bob, Laura)
102+
val pairs: List[(String, Int)] = persons // error
103+
```
104+
We would need an explicit `_.toTuple` selection to express this:
105+
```scala
106+
val pairs: List[(String, Int)] = persons.map(_.toTuple)
107+
```
91108
Note that conformance rules for named tuples are analogous to the rules for named parameters. One can assign parameters by position to a named parameter list.
92109
```scala
93110
def f(param: Int) = ...
@@ -100,8 +117,7 @@ But one cannot use a name to pass an argument to an unnamed parameter:
100117
f(2) // OK
101118
f(param = 2) // Not OK
102119
```
103-
The rules for tuples are analogous. Unnamed tuples conform to named tuple types, but the opposite does not hold.
104-
120+
The rules for tuples are analogous. Unnamed tuples conform to named tuple types, but the opposite requires a conversion.
105121

106122
### Pattern Matching
107123

@@ -327,12 +343,29 @@ By contrast to named tuples, structural types are unordered and have width subty
327343

328344
### Conformance
329345

330-
A large part of Pre-SIP discussion centered around subtyping rules,. whether ordinary tuples should subtype named-tuples (as in this proposal) or _vice versa_ or maybe no subtyping at all.
346+
A large part of Pre-SIP discussion centered around subtyping rules, whether ordinary tuples should subtype named-tuples (as in this proposal) or _vice versa_ or maybe no subtyping at all.
331347

332-
Looking at precedent in other languages it feels like we we do want some sort of subtyping for easy convertibility and possibly an implicit conversion in the other direction.
348+
Looking at precedent in other languages it feels like we we do want some sort of subtyping for easy convertibility and an implicit conversion in the other direction. This proposal picks _unnamed_ <: _named_ for the subtyping and _named_ -> _unnamed_ for the conversion.
333349

334350
The discussion established that both forms of subtyping are sound. My personal opinion is that the subtyping of this proposal is both more useful and safer than the one in the other direction. There is also the problem that changing the subtyping direction would be incompatible with the current structure of `Tuple` and `NamedTuple` since for instance `zip` is already an inline method on `Tuple` so it could not be overridden in `NamedTuple`. To make this work requires a refactoring of `Tuple` to use more extension methods, and the questions whether this is feasible and whether it can be made binary backwards compatible are unknown. I personally will not work on this, if others are willing to make the effort we can discuss the alternative subtyping as well.
335351

352+
_Addendum:_ Turning things around, adopting _named_ <: _unnamed_ for the subtyping and `_unnamed_ -> _named_ for the conversion leads to weaker typing with undetected errors. Consider:
353+
```scala
354+
type Person = (name: String, age: Int)
355+
val bob: Person
356+
bob.zip((firstName: String, agee: Int))
357+
```
358+
This should report a type error.
359+
But in the alternative scheme, we'd have `(firstName: String, agee: Int) <: (String, Int)` by subtyping and then
360+
`(String, Int) -> (name: String, age: Int)` by implicit naming conversion. This is clearly not what we want.
361+
362+
By contrast, in the implemented scheme, we will not convert `(firstName: String, agee: Int)` to `(String, Int)` since a conversion is only attempted if the expected type is a regular tuple, and in our scenario it is a named tuple instead.
363+
364+
My takeaway is that these designs have rather subtle consequences and any alterations would need a full implementation before they can be judged. For instance, the situation with `zip` was a surprise to me, which came up since I first implemented `_.toTuple` as a regular implicit conversion instead of a compiler adaptation.
365+
366+
A possibly simpler design would be to drop all conformance and conversion rules. The problem with this approach is worse usability and problems with smooth migration. Migration will be an issue since right now everything is a regular tuple. If we make it hard to go from there to named tuples, everything will tend to stay a regular tuple and named tuples will be much less used than we would hope for.
367+
368+
336369
### Spread Operator
337370

338371
An idea I was thinking of but that I did not include in this proposal highlights another potential problem with subtyping. Consider adding a _spread_ operator `*` for tuples and named tuples. if `x` is a tuple then `f(x*)` is `f` applied to all fields of `x` expanded as individual arguments. Likewise, if `y` is a named tuple, then `f(y*)` is `f` applied to all elements of `y` as named arguments.

0 commit comments

Comments
 (0)