I’ll have to take your word on this. I have no experience with match-type/variadic-tuple metaprogramming
Update: The proposal was submitted as a SIP
Thinking more on it, I’m not really convinced we need yet another way to create types, especially given the use-cases:
Returning multiple things
As we have established in this thread, it is a non-goal to directly put named tuples in an alias
But this means that if my library goes from having one method returning (x: Int, y: Int)
to having two, I should either duplicate the type, or now make a case class to hold the result
It would have been better to make the case class from the start !
Overall I like the current situation:
- If I want lightweight, I use a tuple, which I document along the method
- If I want safe and/or named, I use a case class
As a user, I always know which one to use
Named tuples would be a third choice, that lives somewhat between the two
Relational algebra
While very exciting, for me this is not a very good reason to add a feature
It would only benefit library authors from one specific domain !
Pattern matching with named fields
I absolutely agree that we want pattern matching with named fields, I am not convinced we need a new feature for them however
We could just add a rule that says unapply
methods can return an UnapplyResult
:
opaque type UnapplyResult[N <: Tuple, +V <: Tuple] >: V = V
Yes, I just renamed NamedTuple
, if it works with syntactic sugar, it’ll work without !
For example, we get the same benefit of being able to coerce a tuple into an UnapplyResult
:
class Person(val name: String, val age: Int)
object Extractor:
def unapply(p: Person): Some[UnapplyResult[("name", "age"), (String, Int)]] =
Some( (p.name, p.age) )
Sure this is more confusing when reading the code, but that’s localized to unapply methods, which are rarely written by end users
Compare this to adding a whole new type that can be present anywhere
Other concerns
- Documentation: currently type aliases (and thus named tuples) cannot carry scaladoc
- Hype: as this is a new exciting feature, people will probably want to put them everywhere, even where another feature is more appropriate
Just want to say one more time: I think it’s a big mistake to have an unnamed tuple be a subtype of every named tuple with the same element types. It’s really counter-intuitive and breaks type-safety. There needs to be a motivating reason besides “otherwise people might have to type names when they don’t feel like it”. Enforcing the names is kind of the point, isn’t it?
To quote Brian Goetz in a tweet from today:
in general, “so people can type fewer keystrokes” is almost always the wrong criteria to prioritize when making decisions
If it’s not going to be the case that named tuples are subtypes of unnamed tuples, then they should be unrelated instead (at least to start with) and people can define their own relationships with implicits. Otherwise, we’ll be locked into this wrong decision and named tuples will be mostly useless.
It would also be really valuable for @milessabin to weigh in on this, given that “named tuples” are treading ground that shapeless has been walking (and paving well-driven highways on) for over a decade – and the subtyping direction of naming in this SIP is opposite that of shapeless. Especially given that tuples are essentially HLists in Scala 3, it seems pretty wise to look toward shapeless records when considering how to add names to them.
If nothing else, just improving the interoperability with SQL databases alone would be of great benefit and would make this feature worth-while.
Would say the biggest oportunity would be for scala.js as it would have a 1:1 construct to structural types, bindings could be more consice, for example scalablytyped generated
Scala.js cannot compile named tuples to JS structural types. They must still have the run-time semantics of regular tuples, and that means not being JS objects.
That makes sense, however if named tuples are closer to js objects than classes, may be simpler to map between them
experimental named tuples support was just merged, but there’s something that bugs me. probably it’s a little too late for bikeshedding, but the renaming in named patterns is somewhat confusing:
locally:
val (x = x, y = y) = (x = 11, y = 22)
assert(x == 11 && y == 22)
locally:
val (x = a, y = b) = (x = 1, y = 2)
assert(a == 1 && b == 2)
(x = 1, y = 2) match
case (x = x, y = y) => assert(x == 1 && y == 2)
here x = a
means “take the value of x
and name it a
”. it’s a complete opposite of normal assignments like val x = a
which means “take the value of a
and name it x
”.
could the renames be written like (x as a, y as b)
instead of (x = a, y = b)
?
here
x = a
means “take the value ofx
and name ita
”. it’s a complete opposite of normal assignments likeval x = a
which means “take the value ofa
and name itx
”.
^ That’s certainly a valid concern
could the renames be written like
(x as a, y as b)
instead of(x = a, y = b)
?
but then how do we square that with the idea that patterns are meant to look identical to constructors, but they play in reverse.
hmm, but do they actually? i assume you mean that deconstruction is meant to look like construction of class instance. in some of the simple forms they do look similar, i.e. when we copy & paste the constructor invocation with parameter names, but without default parameters (unless the parameters are literals, then they can be copied verbatim too). when we start to add other things, like _
, @
or non-literal defaults, then they are different already. i don’t think that increasing the similarity to constructors, beyond simple forms, is more important than the confusion. also creating instance of class is usually done without spelling out parameter names. in summary, i don’t think there’s much of similarity in typical usage anyway, so it shouldn’t be a dominating argument in discussion.
also, when we pass some variable x
to class parameter y
, the relationship between the names shouldn’t matter. if i do:
val input = ???
MyCaseClass(param1 = input)
then having the same name mapping in pattern matching, i.e.:
case MyCaseClass(param1 = input) => ???
is not important. in example above, the name input
could have some significance and make sense in context of object’s construction, but during deconstruction the context is different, so in general there’s no point in replicating the name mapping.
also, to take it from another perspective:
is this written in some official scala language design documents (i.e. how far it should go)? is this elaborated and justified / rationalized somewhere?
i get that regularity of language is very important in scala, but here x = a
can be thought of as an another case of simple assignment too (from cursory look at the source code), therefore changing the regularity argumentation (i.e. similarity to some constructor invocations vs similarity to typical assignments).
Definitely is a bit late, but probably not too late. Experimental phase is meant for experiments and feedback, and possibly changes as a result of thst feedback
Using as
would
- Break the constructor/extractor syntactic symmetry
- Avoid “which side is being bound” confusion
- Provode symmetry with other cases where we bind the name on the right: import statements
import foo.bar as qux
, named pattern bindingsFoo() as foo
, as well as the newly proposedgiven Foo as foo
1 is really bad, 2 and 3 are really good. Really a subjective value judgement here. I can see either way working out.
Maybe @odersky can comment here? Whether or not we end up changing this, I think it’s worth discussing.
The idea that patterns should reflect constructors is indeed a guiding design principle in Scala. The problem with as
is that it binds an optional name to an existing thing. It’s (or rather, should be) an @
in reverse. Example:
case class Tree(left: Tree, right: Tree, elem: Int)
val r: Tree
r match
case Tree(Tree(_, _, 0 as value) as child, _, _) => ...
Here is an example with named pattern matching:
val r: Tree
r match
case Tree(left = Tree(elem = 0)) => ...
Writing as
would not work here.
case Tree(left as Tree(elem as 0)) => ...
The left hand sides of as
are not patterns, and the right hand sides are not names.
I agree that we get a syntactic awkwardness since =
means both “named argument” and assignment. This comes up in named tuples elsewhere as well. For instance,
(a = 3, b = 4)
was a pair of unit result of two assignments but is now a named tuple.
But to remedy the double meaning I think it would be preferable (but probably not realistic) to change assignment to :=
.
that’s not quite as elaborate as i’ve hoped
there are multiple differences already.
hmm, this is not renaming. what i’ve proposed is to use as
keyword in case of renaming only. other usages of =
should stay as they are now.
consider the test cases from the named tuples pull request:
here in x = a
(which is a renaming inside deconstruction) the right side is actually a new name a
and the name x
is hidden by the renaming.
the symmetry with import renaming is particularly strong, so renaming in pattern matching using
as
keyword could then remind users that they can do the same in imports.
// after adding `as`, originalName is gone form scope, localName is added to scope
import pkg.{originalName as localName}
something match {
// after adding `as`, originalName is gone form scope, localName is added to scope
case Something(originalName as localName) =>
}
it goes like this:
val msg = "hello"
val x = Foo(str = msg)
Assign to x the result of calling Foo with argument str assigned to msg, which equals "hello"
then
val Foo(str = msg) = x
assert(msg == "hello")
extract from x the result of Foo extractor, where argument str was assigned from msg, which equals "hello"
hmm, i don’t buy it.
in the first case, in str = msg
the msg
refers to existing stuff, while in second case, in the str = msg
the msg
part is completely new, introduced by pattern matching itself.
they way i see the =
operator (or rather token? to be precise) is that on the right side i always refer to something that already exists, i.e. was already defined and is in scope, and i use it. that’s super regular stuff in any language i know.
below code breaks that ubiquitous regular rule:
Hypothetically speaking, a directional operator like :=
would require the inverted =:
operator for the opposite operation, to remove the ambiguity, as =
is symmetrical
val fu = Foo(str := msg)
val Foo(str =: msg) = fu
As i said hypothetically because the context indicates that it’s a destructure pattern and not assignment, imagine it as if the operator =
was flipped horizontally but unfortunately looks exactly the same
My gut reaction was: That looks so weird.
But thinking more on it, it can be sometimes confusing which of the identifier is the extractor, and which is the parameter name, and that would help in that regard !
Some questions about the current implementation, to try see scala nightly snippet for scala-cli
Based on Named tuples SIP draft
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.
-
Would it be considered a way to store or keep the named literal constant strings at runtime for iteration? it can help when mapping to javascript as js objects key/value pairs are iterable/enumerable, specifically with Object.entries() which returns an array of the key/value pairs of an object.
-
Would it be considered a way to directly get value and value index by string name, similar to
Selectable.selectDynamic
or javascript object field access by nameobj["test"]
Sorry if this sounds too much related to javascript, but that’s where i see they can shine more, as they are liberated from the nominal aspect present case classes while keeping the key/value encapsulation
NOTE: Understanding Named Tuples as only tuples with erased string aliases to indices, then redirects those questions to a different data structure, maybe from shapeless