-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Named tuples second implementation #19174
Conversation
764c3b0
to
3b25712
Compare
Is it possible to change
also the REPL doesn't seem to display tuple literals for some reason.
|
@lrytz We can't change I don't know how to run the repl with a language import in the latest release. Who else can look into this? |
I used
Another observation:
|
val _: (String, Int) = (name = "", age = 0) // error | ||
val _: NameOnly = person // error | ||
val _: Person = nameOnly // error | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
val _: Person = (name = "") ++ nameOnly
library/src/scala/NamedTuple.scala
Outdated
@experimental | ||
object NamedTuple: | ||
|
||
opaque type AnyNamedTuple = Any |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess this separate definition is needed because NamedTuple[?, ?]
is illegal, but this definition is very limiting compared to what can be done with a regular Tuple
:
def elems(x: Tuple): Int =
x.size // ok
def wrap(x: Tuple): Tuple.Map[x.type, List] =
x.map([t] => (x: t) => List(x)) // ok
None of these work on x: AnyNamedTuple
because no extension methods are defined on it.
I can't think of a better way to enable this than writing something like:
opaque type AnyNamedTuple = Tuple
extension (x: AnyNamedTuple)
inline def toTuple: Tuple = x
inline def size: Tuple.Size[Tuple] = toTuple.size
// ...
inline def map[F[_]](f: [t] => t => F[t]): AnyNamedTuple =
toTuple.map(f).asInstanceOf
// ...
and hope that the more precise extension is picked when it is usable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If NamedTuple was covariant in both of its arguments we could maybe write:
opaque type NamedTuple[+N <: Tuple, +V <: Tuple] >: V = V
type AnyNamedTuple = NamedTuple[Tuple, Tuple]
But this breaks the reduction of all match types defined in NamedTuple.scala and upper-bounded by AnyNamedTuple for some reason.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you try AnyNamedTuple = Tuple
? I think I tried that before and it did not work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That doesn't seem to help, somehow all the variants I tried break reduction:
type Person = (name: String, age: Int)
val xx: NamedTuple.Names[Person] = ("name", "age")
// error: Found (String, String) Expected: NamedTuple.Names[Person]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm. It works for me.
97757f7
to
11c65aa
Compare
Some new comments on the older thread on subtyping: #19075 (comment) My recommendation is that we want to be very flexible with tuple convertibility, mirroring what other languages do. So we could have subtyping in one direction and an implicit conversion in the other. Which directions? it comes down to these two questions: val x: (A, B)
val y1: (a1: A, b1: B)
val y2: (a2: A, b2: B)
val z1 = if ??? then x else y1 // -- what is the type of z1?
val z2 = if ??? then y1 else y2 // -- what is the type of z2 Subtyping unnamed <:< named with implicit conversion in the other direction answers as follows: val z1: (a1: A, b1: B)
val z2 // error, untypable [EDIT: Actually, it's AnyNamedTuple] That is, name info is kept when available, type error when it conflicts. When swapping the directions, we get: val z1: (A, B)
val z2: (A, B) That is, name info is erased unless all parts agree on a name. Typescript seems to follow the first approach. I also think it makes more sense. I think it's ergonomically better since it detects errors earlier. |
Shouldn't it be a union of both types |
1db08dc
to
8d73b15
Compare
Actually, widened to |
@sjrd After rebasing to the new match type implementation, I get this:
The error message is not exactly helpful. Can you advise what the problem is and how to fix it? |
The definition of the two match types: /** The names of a named tuple, represented as a tuple of literal string values. */
type Names[X <: AnyNamedTuple] <: Tuple = X match
case NamedTuple[n, _] => n
/** The value types of a named tuple represented as a regular tuple. */
type DropNames[NT <: AnyNamedTuple] <: Tuple = NT match
case NamedTuple[_, x] => x are in a scope that sees the transparent alias case NamedTuple[n, _] => n Since This makes sense because it is equivalent by dealiasing to case ([N, V] =>> V)[n, _] => n and that means it collapses to case _ => n // oops! n is not defined here! The "standard workaround" is to move the definition of these match types to a scope that does not see the transparent alias. This exploits the fact that |
Thanks for the explanation! I'll try that. It would be good to work on the error diagnostics so that one could suggest fixes like this automatically. |
8d73b15
to
1d1cc08
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mostly minor questions and comments. I think the only real issue I have is the subtype relationship. The test cases seems satisfied if we only support using tuple syntax to define a named tuple, looking to get a green PR of that in #19532.
case tp: SingletonType => | ||
if tp.termSymbol == defn.EmptyTupleModule then Some(Nil) else None | ||
if tp.termSymbol == defn.EmptyTupleModule then Some(Nil) | ||
else if normalize then recur(tp.widen, bound) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is this and the SkolemType changes needed?
13c0b17
to
fea6fff
Compare
as is the case for `Concat`
to only require being defined on the element types. Similar to what we have for `type FlatMap`
and refine `type IndexOf` doc
This is for the same reason as we changed `type Head[X <: NonEmptyTuple] = ...` to `type Head[X <: Tuple] = ...` Also, this is no more unsafe than the other operations already defined for all tuples. `drop(1)` for example was always defined, even though `tail` wasn't.
The tuple term level definitions were not being tested before
Keep only the changes we need for making NamedTuple work properly. We still keep operations Contains and Disjoint in Tuple and make Reverse standard. We remove or amend tests that relied on the changes to Tuple. No tests about NamedTuple's are affected.
If we add it as is now, we will *not* be able to add the bound in a future release. It is best to leave it completely undefined. The typer is happy to ignore the case where it does not exist at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
amazing
This implementation follows the alternative representation scheme, where a named tuple type is represented as a
pair of two tuples: one for the names, the other for the values.
Compare with #19075, where named tupes were regular types, with special element types that combine name and value.
In both cases, we use an opaque type alias so that named tuples are represented at runtime by just their values - the names are forgotten.
The new implementation has some advantages
The main disadvantage compared to #19075 is that there is a certain amount of duplication in types and methods between
Tuple
andNamedTuple
. On the other hand, we can make sure by this that no non-sensical tuple operations are accidentally applied to named tuples.