Pre SIP: Named tuples

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.

2 Likes

I saw that Named Tuples will be part of the 3.6 release,and I’m somewhat surprised as it seems to me that the subtyping direction was not at all in consensus
(If anything it seemed like a majority of people on this thread were in favor of the opposite of what is in the SIP)

Could someone describe how the SIP commity thought through these differences, and why this decision was taken ?

1 Like

The SIP has a specific section where Martin explains the reasoning.

And what about breaking the meaning of “assignment” (=)?

This issue was also never further discussed.

Introducing new symbols in scope should really use as, like elsewhere already.

But OK, my issue is “just syntax”. It can be changed after the fact with low effort through automatic rewrite, should it turn out that it’s not only me who thinks that the current syntax created a mayor puzzler.

That’s simply not true. Destructuring assignment is still assignment! There is no “overload of meaning”.

So we have only this super weird and maximally unintuitive special case in named tuples…

1 Like

as is just an abbreviation for assignment. Unfortunate that they didn’t use ass.

But speaking of uniformity of assignment syntax for named args, today I wanted:

Welcome to Scala 2.13.15 (OpenJDK 64-Bit Server VM, Java 23).
Type in expressions for evaluation. Or try :help.

scala> def loop(i: Int): Int = if (i < 100) loop(i=i+1) else 42
def loop(i: Int): Int

scala> def loop(i: Int): Int = if (i < 100) loop(i+=1) else 42
                                                  ^
       error: value += is not a member of Int
         Expression does not convert to assignment because receiver is not assignable.

I know I asked for it once, some time ago. Probably I didn’t get it because I was on the “naughty” list that year.

1 Like

Because there isn’t an issue to begin with.

It’s not a special case because it works exactly the same as every other pattern in the language: take a pattern, replace the binding variables with expressions, and you’ll get an expression that matches the original pattern. E. g. take the pattern (name = x, age = y), substitute "Mateusz" for x and 5 for y in the pattern and you get the expression (name = "Mateusz", age = 5).
It would be unfortunate to deviate from this scheme, and in fact, languages like JavaScript or Haskell which already support this functionality work exactly the same way as Scala does today.

I’m not sure how this is even related.

What you propose in your example is to let assignment evaluate to the assigned value instead of Unit. This was indeed proposed more than once, but it has nothing to do with “uniformity of assignment”.

As long as assignment evaluated to Unit (and also function arguments are final) the current behavior is the only that makes sense. To make your code snipped work a lot of things would need to change in Scala. Because expanding the code under current rules gives:

val $: Int = i + 1
loop(i = $)

and respectively:

val $: Unit = i+=1
loop($)

What should be “uniform” here?

(post deleted by author)

I agree. The subtyping direction could have done with more discussion, but it’s too late now. The direction that was taken seems like it’s out of convenience for a specific use case (i.e. val Laura: Person = ("Laura", 25)) but not a lot of thought was given to whether the semantics make sense.

Now, an ordinary (non-named) tuple is a subtype of every named tuple with equivalent types. This is a really weird situation.

In general, subtyping can be modeled by intersection types – i.e. a subtype can be expressed in a type system that lacks subtypes (but has intersection types) by making it the intersection of the type and all its supertypes. Named tuples are now the exception to that – it’s not possible to express types in that fashion unless tuples are excluded from consideration (along with Nothing).

Just saying, if there’s such a thing as a “code smell” for type systems, it strikes me as one. With all the effort that’s been put into DOT in order to formalize the type system… does this mechanism consistently fit in a formal way?

Edit: In other words, Unnamed <:< Named requires tuples to be “special” in a way that Named <:< Unnamed doesn’t. What we get in return is just fewer keystrokes in some cases (where it’s arguable whether that’s even a good thing). Anyway, what’s done is done.

1 Like

It’s still experimental, though.

One could use the hand break, and keep it experimental for the time being… :slightly_smiling_face:

Somehow the whole question boils down to whether it’s more annoying to sometimes need to write .toTuple, or respectively .widen if the sub-type direction was changed.

The rest is kind of theoretic hair splitting.

I agree that a tuple with names seems more specific than a tuple that has no field names. But our “raw” tuples have actually (generic, synthetic) field names… At that point the discussion starts to be quite murky.

Imho a more interesting question is whether the runtime representation needs to change too if the sub-typing goes in the opposite direction?

My personal mental model of a tuple is that one of a “struct”, and structs don’t have field names at runtime (just offsets). Structs are a compact, not-associative data structure. I think tuples could be used for that in Scala Native, or Scala WASM. But than runtime can’t rely on the field names. So it’s important that the “raw” tuples remain the actual runtime representation.