Named tuples: Bag-like operations

As an early adopter of named tuples, there are situations where I’d like to treat named tuples as a bag of named values rather than a strict sequence of named values.

For example, I’d love to be able to assign tuples that share the same names but in different order,

// could this be valid?  (with the right operators or conversion in scope)
val bag: Bag[(foo: Int, bar: String)] = (bar = "hello", foo = 1)

And in other circumstances, I’d like to pass a set of named values and have the target type decide which ones it’s interested in:

// extract a subset of named values
val bag: Bag[(foo: Int, bar: String)] = (bar = "hello", baz = 1.0, foo = 42)

The resemblance to given’s is intended, in that order in unimportant and selection of available values (based on names instead of types) is desirable. And together, these would bring valuable modularity semantic.

I’m guessing libraries like Chimney could eventually do this, since they offer similar operations on case classes. At the same time, I’m wondering if these would be readily possible through some lightweight type-level programming constructs that may be better hosted in the standard library.

You can already do:

val (foo = foo, bar = bar) = (bar = "hello", foo = 1)

But I guess you already know that anyway.

It seems like what you really want is rather a subset of structural types. But I guess that’s what 99% of people who want named tuples actually want.

2 Likes

The best approximation available is the Selectable trait right?

Yes, I’m aware of Selectable, and it’s going in the right direction with the new Fields type member (mentioned in Scala 3.6.2 is now available! | The Scala Programming Language). But Selectable only helps with the method invocation part of the equation, so is insufficient.

What’s lacking is an algebra on named tuples that provides operations like union/intersection that works like you’d expect on named tuples. Case in point:

type T1 = (foo: Int, bar: String)
type T2 = (bar: String, foo: Int)
val t: T1 | T2 = ...
t.foo // Error: value 'foo' is not a member of T1 | T2

This is indeed where structural types could be helpful in some ways, but named tuples are (AFAIK) not expressible as structural types – at least not currently with type-level programming constructs, without resorting to macros. Maybe macros are the only solution here. I guess I’m back to my original question about whether we should expect this to be developed in the language, or in 3rd-party libraries.

I think the secret lies somewhere in this Scala.IO 2024 talk from the author of the Ducktape library:
https://www.youtube.com/watch?v=I1_dTu2A1r8

specifically a tutorial for how to build a “subfield” operation for named tuple types

3 Likes

The analogy with named args (to function applications) made me want relaxed ordering.

2 Likes

Thank you! Indeed this is very close to it. I’ll see if I can cook something up based on it.

In that case I’m the one percent man. :smile:

I see named tuples as (anonymous) “structs”. Structs have named, ordered fields.

“Unnamed” tuples have also a field order. So this is consistent.

If I wanted structural types I would use structural types… They exist already.


type T1 = (foo: Int, bar: String)
type T2 = (bar: String, foo: Int)
val t: T1 | T2 = ...
t.foo // Error: value 'foo' is not a member of T1 | T2

This OTOH is indeed unlucky.

But it’s consistent with the behavior of classes.

case class T1(foo: Int, bar: String)
case class T2(bar: String, foo: Int)
val t: T1 | T2 = ???
t.foo // Error: value foo is not a member of T1 | T2

That’s more a limitation of Scala’s current union / intersection types and less something named tuple specific.

I think such cases were actually discussed at length when unions / intersections were introduced. Scala just isn’t structurally typed and the current behavior is a consequence of that.

It would be of course nice if we got set operations on object fields, and could create ad-hoc new objects out of the results. But for that we would need structurally typed objects. Something like TS… Also this is not a new idea, but was also part of the discussions regarding | and & types. I’m not sure this will ever exist, though. Scala is nominally typed at the core. Everything else is glued on.

1 Like

There are already fully supported structural types in scala 3, (i.e. scala.Selectable) the problematic part with those is they are harder to create programatically without dipping into macros

For those interested, I got my basic use-cases working:

  • mapping to another named tuple with reordered fields
  • mapping to another named tuple with a subset of fields
4 Likes

Yes but unnamed tuples need an order because there are no names. Once your fields have names and you are using a higher-level language that can manage the low-level memory layout for you, there is no reason why you would care about the order of your fields.

Like som said:

In another thread someone made the analogy between named tuples and named arguments to explain the subtyping relationship. But the thing about named arguments is that once you use them the order in which you pass them does not matter anymore.

2 Likes

“Unordered tuples” wouldn’t be tuples any more. Such a thing is called “record”. But named tuples aren’t records. They are tuples, so they have field order.

Also, what would be the analog of “structs” in environments where memory layout does mater if named tuples were records (which is indeed a higher level data structure than “structs”).

We currently don’t have records in Scala, but we have their types, namely structural types. There are also alternatives to records in Scala, like good old (case) classes. So not sure we need records. But in case, I guess one could build them with the help of that tuplelogy lib @aboisvert created, or likely in some other way. (There were a few proposals for records in the past.)

The point is, one needs first low level structures to build high level structures. A high level structure can’t replace a low level one. Things need to build on each other, from the basic things to the more complex.

That’s why I think named tuples should be seen as tuples, not as records. Tuples are the more basic structure and should come before records.

1 Like