Summary
We propose to add new form of tuples where the elements are named.
type Person = (name: String, age: Int)
val Bob: Person = (name = "Bob", age = 33)
Bob match
case (name, age) =>
println(s"$name is $age years old")
val persons: List[Person] = ...
val minors = persons.filter: p =>
p.age < 18
Named bindings in tuples are similar to function parameters and arguments. We use name: Type
for element types and name = value
for element values. It is illegal to mix named and unnamed elements in a tuple, or to use the same same name for two different elements.
Fields of named tuples can be selected by their name, as in the line p.age < 18
above.
Motivation
-
Tuples are a convenient lightweight way to return multiple results from a function. But the absence of names obscures their meaning, and makes decomposition with
_1
,_2
ugly and hard to read. The existing alternative is to define a class instead. This does name fields, but is more heavy-weight, both in terms of notation and generated bytecode. Named tuples give the same convenience of definition as regular tuples at far better readability. -
Named tuples are an almost ideal substrate on which to implement relational algebra and other database oriented operations. They are a good representation of database rows and allow the definition of generic operations such as projections and joins since they can draw on Scala 3ās existing generic machinery for tuples based on match types.
-
Named tuples make named pattern matching trivial to implement. The discussion on SIP 43 showed that without them itās unclear how to implement named pattern matching at all. With them, the delta in the prototype implementation is about 10 lines of code.
Alternatives considered
We also considered to expand structural types. Structural types allow to abstract over existing classes, but require reflection or some other library-provided mechanism for element access. By contrast, named tuples have a separate representation as tuples, which can be manipulated directly. Since elements are ordered, traversals can be defined, and this allows the definition of type generic algorithms over named tuples. Structural types donāt allow such generic algorithms directly. Be could define mappings between structural types and named tuples, which could be used to implement such algorithms. These mappings would certainly become simpler if they map to/from named tuples than if they had to map to/from user-defined "HMap"s.
By contrast to named tuples, structural types are unordered and have width subtyping. This comes with the price that no natural element ordering exist, and that one usually needs some kind of dictionary structure for access. We believe that the following advantages of named tuples over structural types outweigh the loss of subtyping flexibility:
- Better integration since named tuples and normal tuples share the same representation.
- Better efficiency, since no dictionary is needed.
- Natural traversal order allows the formulation of generic algorithms such as projections and joins.
Details of the Proposal
The proposal has been implemented as an experimental extension of Scala in #19174. The following sections draw from the doc page of that PR.
Conformance
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.
Values of named tuple types can also be be defined using regular tuples. For instance:
val x: Person = ("Laura", 25)
def register(person: Person) = ...
register(person = ("Silvain", 16))
register(("Silvain", 16))
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:
val x: (String, Int) = Bob // error: type mismatch
One can convert a named tuple to an unnamed tuple with the toTuple
method, so the following works:
val x: (String, Int) = Bob.toTuple // ok
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.
def f(param: Int) = ...
f(param = 1) // OK
f(2) // Also OK
But one cannot use a name to pass an argument to an unnamed parameter:
val f: Int => T
f(2) // OK
f(param = 2) // Not OK
The rules for tuples are analogous. Unnamed tuples conform to named tuple types, but the opposite does not hold.
Pattern Matching
When pattern matching on a named tuple, the pattern may be named or unnamed.
If the pattern is named it needs to mention only a subset of the tuple names, and these names can come in any order. So the following are all OK:
Bob match
case (name, age) => ...
Bob match
case (name = x, age = y) => ...
Bob match
case (age = x) => ...
Bob match
case (age = x, name = y) => ...
Expansion
Named tuples are in essence just a convenient syntax for regular tuples. In the internal representation, a named tuple type is represented at compile time as a pair of two tuples. One tuple contains the names as literal constant string types, the other contains the element types. The runtime representation of a named tuples consists of just the element values, whereas the names are forgotten. This is achieved by declaring NamedTuple
in package scala
as an opaque type as follows:
opaque type NamedTuple[N <: Tuple, +V <: Tuple] >: V = V
For instance, the Person
type would be represented as the type
NamedTuple[("name", "age"), (String, Int)]
NamedTuple
is an opaque type alias of its second, value parameter. The first parameter is a string constant type which determines the name of the element. Since the type is just an alias of its value part, names are erased at runtime, and named tuples and regular tuples have the same representation.
A NamedTuple[N, V]
type is publicly known to be a supertype (but not a subtype) of its value paramater V
, which means that regular tuples can be assigned to named tuples but not vice versa.
The NamedTuple
object contains a number of extension methods for named tuples hat mirror the same functions in Tuple
. Examples are
apply
, head
, tail
, take
, drop
, ++
, map
, or zip
.
Similar to Tuple
, the NamedTuple
object also contains types such as Elem
, Head
, Concat
that describe the results of these extension methods.
The translation of named tuples to instances of NamedTuple
is fixed by the specification and therefore known to the programmer. This means that:
- All tuple operations also work with named tuples āout of the boxā.
- Macro libraries can rely on this expansion.
Restrictions
The following restrictions apply to named tuple elements:
- Either all elements of a tuple are named or none are named. It is illegal to mix named and unnamed elements in a tuple. For instance, the following is in error:
val illFormed1 = ("Bob", age = 33) // error
- Each element name in a named tuple must be unique. For instance, the following is in error:
val illFormed2 = (name = "", age = 0, name = true) // error
- Named tuples can be matched with either named or regular patterns. But regular tuples and other selector types can only be matched with regular tuple patterns. For instance, the following is in error:
(tuple: Tuple) match case (age = x) => // error
Syntax
The syntax of Scala is extended as follows to support named tuples:
SimpleType ::= ...
| ā(ā NameAndType {ā,ā NameAndType} ā)ā
NameAndType ::= id ':' Type
SimpleExpr ::= ...
| '(' NamedExprInParens {ā,ā NamedExprInParens} ')'
NamedExprInParens ::= id '=' ExprInParens
SimplePattern ::= ...
| '(' NamedPattern {ā,ā NamedPattern} ')'
NamedPattern ::= id '=' Pattern