Pre SIP: Named tuples

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

4 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:

3 Likes

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

Given that named tuples types are declared using type aliases, does it make sense to allow recursive types, currently this is invalid code

type Person = (name: String, age: Int, friend: Option[Person])
3 Likes

May also be related, the possibility to directly serialize and deserialize to JSON Objects, which may not be possible if names are totally erased

this one is possible as long as the type is known at the call-site (you can use constValueTuple method to reflect the names to a value) e.g.

inline def pairs[N <: Tuple, V <: Tuple](nt: NamedTuple[N, V]): Unit =
  val names = constValueTuple[N]
  names.zip(nt.toTuple).toList.foreach(println)

scala> pairs((x = 3, y = 7))
(x,3)
(y,7)
2 Likes

There are some discussions over here Pre-SIP: a syntax for aggregate literals - #107 by lihaoyi about the usefulness of partially named tuples in relation to scrapping boilerplate of constructors etc. Would the current Named tuples scheme be possible to extend to partially named tuples and be unified with (some of) the ideas in that thread?

I believe it would be a considerable can of worms but that’s more a gut feeling than backed by data. I’d not try to extend the named tuples proposal with this at this time. It could be added later if needed.

2 Likes

OK, yes can be considered later, if not to hairy. Thanks.

I really like the fact that my “aggregate literal” proposal does not add any new first class concepts to the language (which might interact with other language features in all sorts of ways) but only allows us to write the code that we’re already writing today with less boilerplate. So color me quite sceptical about this idea.

the purpose of it is computing structural types, and to give better meaning to temporary return values - neither of which are addressed by that proposal

4 Likes

I’ve read through the current version of the feature. I very much welcome it!

It’s simple at the core, it prioritizes imho the right things (Tuple :< NamedTuple, with a conversion the other way around), it doesn’t make named tuples some weird kind of hashmaps / objects (order matters!) in the first place (this is imho important as tuples are very basic building blocks for data, much closer to “structs” than to “objects”; “anonymous objects” should be a higher level feature imho), and they’re even super lightweight when it comes to runtime cost. I really like the design. It will imho also make a great base for other features on top in the future! (Maybe we get better usable CStructs in Scala Native, for example?)

But there is one single thing that cough my attention. I think it’s actually a hot candidate for a all time high-score on Scala Puzzlers. It’s the renaming in pattern matching. That behaves just completely unexpected. I had to read this part a few times until I believed that I got it. Very unintuitive.

In this regard I fully support @tarsa’s older remarks to which I answer.

I would really hope the syntax for this one tiny peace of functionality gets changed.

I would really like to the see the proposed as syntax instead.

We established already in some other threads that as has well formed “meta meaning”. It stands for a “rename that introduces a fresh symbol”. That works for import renaming, that works for naming given instances, that works in the other pattern matching proposal. It would also work for this case here imho.

I understand that this would create some “irregularity” in a (quite seldom used form of) pattern matching, but it would save the basic meaning of =, which is clearly assignment.

So please consider this small change request.

Everything else looks great, but this tiny piece looks just odd. It really puzzled me.

It behaves exactly like it should: patterns look the same as expressions, except that in a pattern you can replace a subexpression with an identifier in order to bind said identifier. It would be unfortunate to deviate from this principle.

Actually = is overloaded in Scala and has more than one meaning. One use of = is in val declarations to separate the pattern from the initializer. The named tuple syntax is another, separate thing that just happens to use the same symbol, and this can be clearly seen from languages like JavaScript that use different symbols for the two: = for pattern binding, i. e. const [a, b] = [1, 2], and : for object literals, e. g. {a: 1, b: 2}.

1 Like

I’m not a fan of either named parameter or named tuple, because they are not hygienic, sometime not hygienic is confusing. For example:

def NamedParameterFunction(a: Int, b: Int) = { ... }

val a = 1, b = 2
NamedParameterFunction(b, a)
NamedParameterFunction(a, b)
NamedParameterFunction(a = b, b = a)

What’s the behavior, may be it is too clear for scala expert, but it do increase the learning curve.

Named tuple may intruduce more confusion, for example:

type Something = (a: Int, b: Int)

val something: Something
something match {
  case (b, a) => // what's the behavior ? it is too confusing.
                 // In a hygienic world, both a and b are new identifier here, 
                 // which should be totally different from Something.a or
                 // Something.b, no confusion at all.
}

A member name of a class should only be valid inside the class, when refering the name outside the class, class name or class object should be used instead of that name solely. So case class is always a better solution.