Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make named tuples a standard feature #21680

Merged
merged 5 commits into from
Oct 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions compiler/src/dotty/tools/dotc/config/Feature.scala
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ object Feature:
val pureFunctions = experimental("pureFunctions")
val captureChecking = experimental("captureChecking")
val into = experimental("into")
val namedTuples = experimental("namedTuples")
val modularity = experimental("modularity")
val betterMatchTypeExtractors = experimental("betterMatchTypeExtractors")
val quotedPatternsWithPolymorphicFunctions = experimental("quotedPatternsWithPolymorphicFunctions")
Expand Down Expand Up @@ -66,7 +65,6 @@ object Feature:
(pureFunctions, "Enable pure functions for capture checking"),
(captureChecking, "Enable experimental capture checking"),
(into, "Allow into modifier on parameter types"),
(namedTuples, "Allow named tuples"),
(modularity, "Enable experimental modularity features"),
(betterMatchTypeExtractors, "Enable better match type extractors"),
(betterFors, "Enable improvements in `for` comprehensions")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ object ScalaSettingsProperties:
ScalaRelease.values.toList.map(_.show)

def supportedSourceVersions: List[String] =
SourceVersion.values.toList.map(_.toString)
(SourceVersion.values.toList.diff(SourceVersion.illegalSourceVersionNames)).toList.map(_.toString)

def supportedLanguageFeatures: List[ChoiceWithHelp[String]] =
Feature.values.map((n, d) => ChoiceWithHelp(n.toString, d))
Expand Down
6 changes: 6 additions & 0 deletions compiler/src/dotty/tools/dotc/inlines/Inliner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -957,6 +957,12 @@ class Inliner(val call: tpd.Tree)(using Context):
case None => tree
case _ =>
tree

/** For inlining only: Given `(x: T)` with expected type `x.type`, replace the tree with `x`.
*/
override def healAdapt(tree: Tree, pt: Type)(using Context): Tree = (tree, pt) match
case (Typed(tree1, _), pt: SingletonType) if tree1.tpe <:< pt => tree1
case _ => tree
end InlineTyper

/** Drop any side-effect-free bindings that are unused in expansion or other reachable bindings.
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/parsing/Parsers.scala
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ object Parsers {
else leading :: Nil

def maybeNamed(op: () => Tree): () => Tree = () =>
if isIdent && in.lookahead.token == EQUALS && in.featureEnabled(Feature.namedTuples) then
if isIdent && in.lookahead.token == EQUALS && sourceVersion.isAtLeast(`3.6`) then
atSpan(in.offset):
val name = ident()
in.nextToken()
Expand Down Expand Up @@ -2137,7 +2137,7 @@ object Parsers {

if namedOK && isIdent && in.lookahead.token == EQUALS then
commaSeparated(() => namedArgType())
else if tupleOK && isIdent && in.lookahead.isColon && in.featureEnabled(Feature.namedTuples) then
else if tupleOK && isIdent && in.lookahead.isColon && sourceVersion.isAtLeast(`3.6`) then
commaSeparated(() => namedElem())
else
commaSeparated(() => argType())
Expand Down
13 changes: 11 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -789,7 +789,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
def tryNamedTupleSelection() =
val namedTupleElems = qual.tpe.widenDealias.namedTupleElementTypes
val nameIdx = namedTupleElems.indexWhere(_._1 == selName)
if nameIdx >= 0 && Feature.enabled(Feature.namedTuples) then
if nameIdx >= 0 && sourceVersion.isAtLeast(`3.6`) then
typed(
untpd.Apply(
untpd.Select(untpd.TypedSplice(qual), nme.apply),
Expand Down Expand Up @@ -4602,7 +4602,10 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer

def recover(failure: SearchFailureType) =
if canDefineFurther(wtp) || canDefineFurther(pt) then readapt(tree)
else err.typeMismatch(tree, pt, failure)
else
val tree1 = healAdapt(tree, pt)
if tree1 ne tree then readapt(tree1)
else err.typeMismatch(tree, pt, failure)

pt match
case _: SelectionProto =>
Expand Down Expand Up @@ -4751,6 +4754,12 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
}
}

/** Hook for inheriting Typers to do a last-effort adaptation. If a different
* tree is returned, we will re-adapt that one, otherwise we issue a type error afterwards.
``
*/
protected def healAdapt(tree: Tree, pt: Type)(using Context): Tree = tree

/** True if this inline typer has already issued errors */
def hasInliningErrors(using Context): Boolean = false

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
layout: doc-page
title: "Named Tuples"
nightlyOf: https://docs.scala-lang.org/scala3/reference/experimental/named-tuples.html
nightlyOf: https://docs.scala-lang.org/scala3/reference/other-new-features/named-tuples.html
---

The elements of a tuple can now be named. Example:
Starting in Scala 3.6, the elements of a tuple can be named. Example:
```scala
type Person = (name: String, age: Int)
val Bob: Person = (name = "Bob", age = 33)
Expand Down Expand Up @@ -94,6 +94,24 @@ Bob match
case (age = x, name = y) => ...
```

### Pattern Matching with Named Fields in General

We allow named patterns not just for named tuples but also for case classes. For instance:
```scala
city match
case c @ City(name = "London") => println(c.population)
case City(name = n, zip = 1026, population = pop) => println(pop)
```

Named constructor patterns are analogous to named tuple patterns. In both cases

- every name must match the name some field of the selector,
- names can come in any order,
- not all fields of the selector need to be matched.

Named patterns are compatible with extensible pattern matching simply because
`unapply` results can be named tuples.

### 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`
Expand All @@ -119,6 +137,47 @@ The translation of named tuples to instances of `NamedTuple` is fixed by the spe
- All tuple operations also work with named tuples "out of the box".
- Macro libraries can rely on this expansion.

### Computed Field Names

The `Selectable` trait now has a `Fields` type member that can be instantiated
to a named tuple.

```scala
trait Selectable:
type Fields <: NamedTuple.AnyNamedTuple
```

If `Fields` is instantiated in a subclass of `Selectable` to some named tuple type,
then the available fields and their types will be defined by that type. Assume `n: T`
is an element of the `Fields` type in some class `C` that implements `Selectable`,
that `c: C`, and that `n` is not otherwise legal as a name of a selection on `c`.
Then `c.n` is a legal selection, which expands to `c.selectDynamic("n").asInstanceOf[T]`.

It is the task of the implementation of `selectDynamic` in `C` to ensure that its
computed result conforms to the predicted type `T`

As an example, assume we have a query type `Q[T]` defined as follows:

```scala
trait Q[T] extends Selectable:
type Fields = NamedTuple.Map[NamedTuple.From[T], Q]
def selectDynamic(fieldName: String) = ...
```

Assume in the user domain:
```scala
case class City(zipCode: Int, name: String, population: Int)
val city: Q[City]
```
Then
```scala
city.zipCode
```
has type `Q[Int]` and it expands to
```scala
city.selectDynamic("zipCode").asInstanceOf[Q[Int]]
```

### The NamedTuple.From Type

The `NamedTuple` object contains a type definition
Expand All @@ -137,33 +196,36 @@ then `NamedTuple.From[City]` is the named tuple
(zip: Int, name: String, population: Int)
```
The same works for enum cases expanding to case classes, abstract types with case classes as upper bound, alias types expanding to case classes
and singleton types with case classes as underlying type.
and singleton types with case classes as underlying type (in terms of the implementation, the `classSymbol` of a type must be a case class).

`From` is also defined on named tuples. If `NT` is a named tuple type, then `From[NT] = NT`.


### Operations on Named Tuples

The operations on named tuples are defined in object [scala.NamedTuple](https://www.scala-lang.org/api/3.x/scala/NamedTuple$.html).

### Restrictions

The following restrictions apply to named tuple elements:
The following restrictions apply to named tuples and named pattern arguments:

1. 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:
1. Either all elements of a tuple or constructor pattern 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:
```scala
val illFormed1 = ("Bob", age = 33) // error
```
2. Each element name in a named tuple must be unique. For instance, the following is in error:
2. Each element name in a named tuple or constructor pattern must be unique. For instance, the following is in error:
```scala
val illFormed2 = (name = "", age = 0, name = true) // error
```
3. 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:
3. Named tuples and case classes 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:
```scala
(tuple: Tuple) match
case (age = x) => // error
```
4. Regular selector names `_1`, `_2`, ... are not allowed as names in named tuples.
## Syntax Changes

### Syntax

The syntax of Scala is extended as follows to support named tuples:
The syntax of Scala is extended as follows to support named tuples and
named constructor arguments:
```
SimpleType ::= ...
| ‘(’ NameAndType {‘,’ NameAndType} ‘)’
Expand All @@ -178,31 +240,11 @@ Patterns ::= Pattern {‘,’ Pattern}
NamedPattern ::= id '=' Pattern
```

### Named Pattern Matching

We allow named patterns not just for named tuples but also for case classes.
For instance:
```scala
city match
case c @ City(name = "London") => println(p.population)
case City(name = n, zip = 1026, population = pop) => println(pop)
```

Named constructor patterns are analogous to named tuple patterns. In both cases

- either all fields are named or none is,
- every name must match the name some field of the selector,
- names can come in any order,
- not all fields of the selector need to be matched.

This revives SIP 43, with a much simpler desugaring than originally proposed.
Named patterns are compatible with extensible pattern matching simply because
`unapply` results can be named tuples.

### Source Incompatibilities

There are some source incompatibilities involving named tuples of length one.
First, what was previously classified as an assignment could now be interpreted as a named tuple. Example:

```scala
var age: Int
(age = 1)
Expand All @@ -221,43 +263,3 @@ c f (age = 1)
```
will now construct a tuple as second operand instead of passing a named parameter.

### Computed Field Names

The `Selectable` trait now has a `Fields` type member that can be instantiated
to a named tuple.

```scala
trait Selectable:
type Fields <: NamedTuple.AnyNamedTuple
```

If `Fields` is instantiated in a subclass of `Selectable` to some named tuple type,
then the available fields and their types will be defined by that type. Assume `n: T`
is an element of the `Fields` type in some class `C` that implements `Selectable`,
that `c: C`, and that `n` is not otherwise legal as a name of a selection on `c`.
Then `c.n` is a legal selection, which expands to `c.selectDynamic("n").asInstanceOf[T]`.

It is the task of the implementation of `selectDynamic` in `C` to ensure that its
computed result conforms to the predicted type `T`

As an example, assume we have a query type `Q[T]` defined as follows:

```scala
trait Q[T] extends Selectable:
type Fields = NamedTuple.Map[NamedTuple.From[T], Q]
def selectDynamic(fieldName: String) = ...
```

Assume in the user domain:
```scala
case class City(zipCode: Int, name: String, population: Int)
val city: Q[City]
```
Then
```scala
city.zipCode
```
has type `Q[Int]` and it expands to
```scala
city.selectDynamic("zipCode").asInstanceOf[Q[Int]]
```
2 changes: 1 addition & 1 deletion docs/sidebar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ subsection:
- page: reference/other-new-features/export.md
- page: reference/other-new-features/opaques.md
- page: reference/other-new-features/opaques-details.md
- page: reference/other-new-features/named-tuples.md
- page: reference/other-new-features/open-classes.md
- page: reference/other-new-features/parameter-untupling.md
- page: reference/other-new-features/parameter-untupling-spec.md
Expand Down Expand Up @@ -154,7 +155,6 @@ subsection:
- page: reference/experimental/cc.md
- page: reference/experimental/purefuns.md
- page: reference/experimental/tupled-function.md
- page: reference/experimental/named-tuples.md
- page: reference/experimental/modularity.md
- page: reference/experimental/typeclasses.md
- page: reference/experimental/runtimeChecked.md
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package scala
import scala.language.experimental.clauseInterleaving
import annotation.experimental
import compiletime.ops.boolean.*

@experimental
object NamedTuple:

/** The type to which named tuples get mapped to. For instance,
Expand Down Expand Up @@ -133,7 +130,6 @@ object NamedTuple:
end NamedTuple

/** Separate from NamedTuple object so that we can match on the opaque type NamedTuple. */
@experimental
object NamedTupleDecomposition:
import NamedTuple.*
extension [N <: Tuple, V <: Tuple](x: NamedTuple[N, V])
Expand Down
1 change: 1 addition & 0 deletions library/src/scala/runtime/stdLibPatches/language.scala
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ object language:
* @see [[https://dotty.epfl.ch/docs/reference/experimental/named-tuples]]
*/
@compileTimeOnly("`namedTuples` can only be used at compile time in import statements")
@deprecated("The experimental.namedTuples language import is no longer needed since the feature is now standard", since = "3.6")
object namedTuples

/** Experimental support for new features for better modularity, including
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1988,8 +1988,7 @@ class CompletionSuite extends BaseCompletionSuite:

@Test def `namedTuple completions` =
check(
"""|import scala.language.experimental.namedTuples
|import scala.NamedTuple.*
"""|import scala.NamedTuple.*
|
|val person = (name = "Jamie", city = "Lausanne")
|
Expand All @@ -2000,8 +1999,7 @@ class CompletionSuite extends BaseCompletionSuite:

@Test def `Selectable with namedTuple Fields member` =
check(
"""|import scala.language.experimental.namedTuples
|import scala.NamedTuple.*
"""|import scala.NamedTuple.*
|
|class NamedTupleSelectable extends Selectable {
| type Fields <: AnyNamedTuple
Expand Down Expand Up @@ -2091,7 +2089,7 @@ class CompletionSuite extends BaseCompletionSuite:
|""".stripMargin
)

@Test def `conflict-3` =
@Test def `conflict-3` =
check(
"""|package a
|object A {
Expand Down
14 changes: 7 additions & 7 deletions tests/neg/i20517.check
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
-- [E007] Type Mismatch Error: tests/neg/i20517.scala:10:43 ------------------------------------------------------------
10 | def dep(foo: Foo[Any]): From[foo.type] = (elem = "") // error
| ^^^^^^^^^^^
| Found: (elem : String)
| Required: NamedTuple.From[(foo : Foo[Any])]
|
| longer explanation available when compiling with `-explain`
-- [E007] Type Mismatch Error: tests/neg/i20517.scala:9:43 -------------------------------------------------------------
9 | def dep(foo: Foo[Any]): From[foo.type] = (elem = "") // error
| ^^^^^^^^^^^
| Found: (elem : String)
| Required: NamedTuple.From[(foo : Foo[Any])]
|
| longer explanation available when compiling with `-explain`
1 change: 0 additions & 1 deletion tests/neg/i20517.scala
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import scala.language.experimental.namedTuples
import NamedTuple.From

case class Foo[+T](elem: T)
Expand Down
2 changes: 0 additions & 2 deletions tests/neg/named-tuple-selectable.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import scala.language.experimental.namedTuples

class FromFields extends Selectable:
type Fields = (i: Int)
def selectDynamic(key: String) =
Expand Down
Loading
Loading