Named Tuple Arguments / Anonymous Case Classes

I often find myself in a dilemma when creating a helper function that returns a tuple.
On the one hand, I don’t want to create a case class specially for this single function, while on the other hand it’s more difficult to understand/use the return arguments because they are unnamed (_1, _2, ...)

private def getNames[T] : (String, Map[T, String]) = ???

I propose that it will be possible to name the arguments for these tuples, effectively making them anonymous case classes.

private def getNames[T] : (ownerName : String, memberNameMap : Map[T, String]) = ???

will be equivalent to

final case class $anon[T](ownerName : String, memberNameMap : Map[T, String])
private def getNames[T] : $anon[T] = ???

Bonus: This will also help properly document the unapply return types which gets confusing in custom unapply pattern matching.

16 Likes

It seems like a waste to create anonymous classes for this. It would make Scala generate even more bloated JVM bytecode.

I think a better approach would be to allow naming the components of tuples for documentation, a purely static abstraction which would be completely erased at runtime.

Example:

val p: (x: Int, y: Int, z: Int) = (1, 2, 3)
// ^ we have (Int, Int, Int) <: (x: Int, y: Int, z: Int)

println(p.x) // implicit extension to allow accessing the fields by name

val q: (Int, Int, Int) = p
// ^ we have (x: Int, y: Int, z: Int) <: (Int, Int, Int)
9 Likes

It would be nice if tuples could be used without any overhead and the names are simply “syntactic sugar” for accessing _1 or _2. However, the names should be declared at the definition site and not at the usage site. I fear it would mean a larger effort for the compiler to “transport” these names to the usage sites.

2 Likes

IMO it should be very close to C# named tuples + tuple projection initializers so that I can write:

val t = (name, age)
t.age
11 Likes

I agree. Your proposal is better. However, if the implementation relies on desugaring, then we could have ambiguous extensions:

val p: (x: Int, y: Int, z: Int) = (1, 2, 3)
val p2: (a: Int, b: Int, c: Int) = (1, 2, 3)

Technically, members of a tuple are not unnamed. _1 etc are names. So, what you want is an alias. We have import to do that. We can use import to rename tup._1 to, say x.


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

> val tup = (1, 2)
val tup: (Int, Int) = (1,2)

> import tup.{_1 => x }
import tup.{_1=>x}

> x
val res0: Int = 1

I think we cannot rename tup._1 to tup.x using import, but maybe we can extend import to enable that?

1 Like

Not sure I see the problem in your example. p would have extensions available for x, y, and z, and p2 would have extensions for a, b, and c.

I’m thinking of extensions which are handled by the compiler directly, or implemented with a macro and the Dynamic trait (also possible). Not actual concrete extension classes. As @curoli said, they would desugar to calls to _1, _2 & co.

1 Like

I though you meant that the desugaring will be like

implicit class pExt(p : (Int, Int, Int)) {
  def x = p._1
  def y = p._2
  def z = p._3
}

And my example was bad since it missed a conflicting named argument.

val p: (x: Int, y: Int, z: Int) = (1, 2, 3)
val p2: (z: Int, y: Int, x: Int) = (1, 2, 3)

So what I figure is that in any case the compiler needs to handle this directly and not desugar it.

2 Likes

Ah but my point was to avoid generating useless classes; generating an implicit class instead of a named tuple class would not help with that :^)

It would still be mostly desugared (named accesses would become _n accesses), but yeah the compiler would need to have some dedicated representation of these named tuples for it to work.

3 Likes

Yeah, and they should also work for named pattern matching, if that will be implemented in the future.

5 Likes

My 2 cents:

  1. Personally I think it’s fine to just create the case classes manually, do we really need compiler magic? I started off loving all the invisible work you could do in Scala (implicit everything and macros) but now that I’ve been using it for so long, I strongly feel that explicit is better for everyone (including yourself in the future, just not yourself as you’re typing).

  2. If it’s generated, it shouldn’t be anonymous IMO. I’ve done this before and it’s very useful (not just for return types, but for the composite of all the arguments too) and I’ve found that you often end up explicitly referring to the result-class. Eg. you might not consume the result immediately but pass it to some other function that expects to work with it, and to do that, it needs to accept the result-class as one of its arguments. So instead of an anonymous class how about something like s"${methodName}Result" or something?

tl;dr: I feel weakly that generation harms learnability and readability for little gain (sorry OP!), and I feel strongly that if we generate the classes they shouldn’t be anonymous :slight_smile:

PS: to clarify what I meant about the same being a useful technique for arguments too. Imagine you had a function def blah(a1,a2,a3...), that takes 10 args; instead of doing it the typical way you can create a case class BlahArgs with all 10 values and change the function to def blah(args: BlahArgs), which means:

  • if you’re passing them around between functions/layers, you don’t have to mirror the same arg-decl-code everywhere which is noisy and actually abstraction leak if you think about it
  • similarly you can add new args and not have to modify any boilerplate - the functions that just pass them through need no change
  • you can have functions that generate/prepare, and then return the args
3 Likes

I need something which helps me to prevent errors and boilerplate code:

val q3 = for {
  c <- coffees if c.price < 9.0
  s <- c.supplier
} yield (
  c.name, 
  s.name
)

It is look cool. But our usual table contains more than 20 attributes.
And such expressions will be very error prone after 5 attributes and more. Especially when you need refactoring. Creating named tuples manually is like creating many anonymous classes. It leads to boilerplate code. Unfortunately the classes cannot be created in function body, so it breaks context and make code more difficult.

Is tuple really the right abstraction here? Once you introduce field names, order does not matter anymore and you should have a width subtype relation. Structural types give you all that, so should we not rather try to build on those?

1 Like

When referencing a named argument, indeed the order does not matter, but if we wish to support a native tuple assignment like shown by @LPTK and pattern matching, then the order does matter.

When we are sorting or filtering large array of data, access by name has significant value in our profiler tests.
Currently we use pojo\case classes for cases where we need optimization, choosing between several approach(more simple or more fast) is also a some sort of headache.

I’m not familiar with C#, but I believe the relevant example is:

public static double StandardDeviation(IEnumerable<double> sequence)
{
    var computation = ComputeSumAndSumOfSquares(sequence);

    var variance = computation.SumOfSquares - computation.Sum * computation.Sum / computation.Count;
    return Math.Sqrt(variance / computation.Count);
}

private static (int Count, double Sum, double SumOfSquares) ComputeSumAndSumOfSquares(IEnumerable<double> sequence)
{
...
}

It’s worth noting that TypeScript 4.0 adds labeled tuple elements

3 Likes

@soronpo it’s also important to notice that the name labels in TypeScript tuples do not generate field accessors. They’re just syntactical metadata used to improve readability and some IDE features.

@gabro it’s also also important to note the reason TypeScript cannot allow field-access syntax on these: it’s because TS is tied to JS semantics, i.e., TS code must be just JS with types — in particular, a field selection in TS must correspond to a field selection in JS. There is no runtime fields actually recorded in the tuple runtime values, so a field selection to access them in TS would not make sense.

However, in Scala we can rely on elaboration, a process which happens during type checking, and which TypeScript completely lacks. It’s the process which transforms d.foo into d.selectDynamic("foo") if d is an instance of Dynamic, for instance, among many other things.

Similarly, we could just as easily transform tuple.foo into tuple._2 if tuple has a type like (bar: Int, foo: String, baz: Boolean).

4 Likes

For sure we could, I’m just pointing out that the motivations behind that feature in TS are very different than the change proposed here, so I’m not sure it’s relevant :slight_smile:

That said TS still has some features that introduce runtime changes, such as enums, but it’s also true that they’re actively moving away from that approach

1 Like