Pre SIP: Named tuples

I’ll have to take your word on this. I have no experience with match-type/variadic-tuple metaprogramming

1 Like

Update: The proposal was submitted as a SIP

2 Likes

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
2 Likes

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.

3 Likes

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.

4 Likes

If nothing else, just improving the interoperability with SQL databases alone would be of great benefit and would make this feature worth-while.

1 Like

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.

1 Like

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)?

1 Like

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 ”.

^ 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.

2 Likes

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).

2 Likes

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

  1. Break the constructor/extractor syntactic symmetry
  2. Avoid “which side is being bound” confusion
  3. Provode symmetry with other cases where we bind the name on the right: import statements import foo.bar as qux, named pattern bindings Foo() as foo, as well as the newly proposed given 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.

2 Likes

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 :=.

3 Likes

that’s not quite as elaborate as i’ve hoped :slight_smile:
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.

:+1: 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"

4 Likes

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:

1 Like

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.

  1. 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.

  2. 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 name obj["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