This has been a long thread, and it has also been accompanied by extensive â hours-long â discussions off-line back here at EPFL. It is time that I provide my analysis and opinion. There are several broad areas I want to touch upon:
- Motivation: use cases, motivating example, benefit over case classes
- Migration story
- The infamous subtyping direction
I will post each of these areas as separate posts, because I suspect that different sets of people will want to likeâor not likeâthem separately.
Use cases
Often, SIPs and Pre-SIPS start with terrible motivating examples. Usually this is because they focus on simple âhow does it workâ examples rather than âwhy would I use itâ examples. This Pre-SIP is no exception. âWhy would I use itâ examples are typically longer but much more important.
I see 3 areas where named tuples bring substantial value over what we currently have:
- Methods returning multiple result
- Enabler for named pattern matching
- Data type for intermediate results in operations that compute types (notably, database-like manipulations)
Multiple-result methods
Methods that return multiple results are not rare. In the collections library, for example, we can find partition
, span
, splitAt
, partitionMap
. These methods really want to return two results. They use a tuple to do so because thatâs what the language offers to do so, not because the result is semantically a pair. (Contrast with unzip
or unzip3
, for which the result being a tuple makes inherent sense.)
Iâll choose partition
as the canonical example. It is a good example because Iâve seen on numerous occasions developers saying that they never remember which side is which:
val (underagePersons, offAgePersons) = persons.map(_.age >= 18)
// oops, got it wrong
The confusion is not surprising: there is no fundamental reason that matching elements should go to the left and non-matching elements should go the right. The Saladoc of course says which is which, but it would be better if the API itself would provide the information.
This would naturally be achieved with named tuples. We would define partition
as:
def partition(p: A => Boolean): (matching: List[A], nonMatching: List[A]) = ???
and then the confusion would immediately disappear.
Note that in argument position, we would use separate parameters for this, each with its own name. We only do this in result position because we have no other choice.
Also note that the named tuple type is used exactly once. Thatâs because itâs barely a type at all; it is almost only part of that single methodâs signature. Defining a case class for that would make no sense.
Enabler for named pattern matching
Named pattern matching is very desirable. It has been coming up regularly over the years. Why has Scala never added support for it? Because we never figured out we could do it. The latest SIP on the topic came close, but did not succeed in the end.
With named tuples, we finally have a good answer to named pattern matching: if an extractorâs unapply
method returns a named tuple or an Option
of a named tuple, we can use its names in the pattern. For example:
object Duration {
def unapply(s: String): Option[(length: Long, unit: TimeUnit)] = = ...
}
input match {
case Duration(length = len, unit = TimeUnit.Seconds) => s"$len seconds"
}
Once again, this type is written exactly once: in the signature of the unapply
method. It would never need a type alias or any other kind of explicit name.
This use case cannot be replaced by case classes. If we tried to do that, we would not be able to explain case classes in terms of other language features. They would have to be truly magic, and that is something we always wanted to avoid.
Computed intermediate types
I am not myself a user of database operations or any other thing like that, so I will refrain from motivating why we want those operations in the first place. However, using named tuples for them is a true enabler.
Operations like join
s take two sets of rows and produce a new set of rows. The unique aspect here is that the resulting type can be generically computed from the types of the inputs.
Type computations cannot create classes (nor traits or any other form of nominal types). However, they can produce new types that structurally compose other types. This applies whether or not the type computations are in the language (e.g., with match types) or in macros. Therefore, using case classes here is also a complete no-go.
Existing solutions try to use structural types, but these are notoriously difficult to handle. In particular, their unordered nature makes it sketchy to destructure and compute upon (although we can construct them).
Named tuples, with their ordered, static list of name-value pairs provide a unique solution to this category of problems.
Anti use cases
As several people have already observed, as soon as you have to define a type
alias for your named tuple, the usefulness compared to case classes is debatable at best. I will go as far as to say it is actively harmful, for several reasons:
- Lack of a place to put associated documentation for each field,
- Potential to mix and match, by mistake, two types that are structurally the same (but not semantically), and
- The sheer decision factor of having to choose between case classes and named tuples.
Therefore, in my opinion, defining a type alias over a named tuple should be seen as a code smell. It may happen very sporadically in some situations (every code smell has its exceptions), but the overwhelming majority of cases should not do that. It might be good to lint against it. It certainly does not serve this proposal that the explainer puts this âuse caseâ forward. We should never show this; not encourage developers to do it by giving them bad examples like that.
The fact that this is an anti-use-case does not undermine the value of the actual, good use cases I have elaborated on above, though.