Syntax for type tuple with one element

This doesn’t work for the same reason that Some[T] = T | null doesn’t: It doesn’t compose.

What happens when you want a Tuple1[Tuple1[T]] ?
If you know about it statically, it might be fine, but you might have things like:
([T] => Tuple1[T])[Tuple1[A]]
And these applications might be in different compilation units !

I fully relate to the intuition however, it seems like it would be such a clean solution, but reality sadly disagrees with our gut feeling in this case

A non-technical reason is that for better or for worse Unit is now associated with side-effects (otherwise, what would be the point to call something which returns nothing !)

Ensuring we cannot pass a Unit as a Tuple helps making sure people don’t use side-effecting methods when they do not intend to, for example:

var t1 = (1, 2)

// updates in-place or creates updated copy ?
val t2: Tuple = myUpdate(t1, 0, 4)

As a consequence, please don’t write side-effecting methods returning EmptyTuple !

There is a surprisingly simple solution to all the tuple syntax trouble.

But I don’t know whether you’ll like it. Despite it’s coming even with a small quality of live improvement.

The idea is:

One could simply disallow parenthesis for grouping.

Braces would become mandatory for grouping.

Consequently one would end up with only one canonical Unit literal, namely {}, as “grouping nothing-really into an expression” results in exactly this expression, an empty expression, which is by definition of type Unit.

Using braces for grouping, and using {} as Unit literal works already as of today.

One would just need to rewrite syntax; which can happen fully automatic as the compiler recognizes Unit literals and grouping parens.

Doing that would than free the parens syntax exclusively for Tuple literals.

Than () is definitely an empty tuple. (()) would be a Tuple(Tuple()).

One would have also literal syntax for things like

Tuple(Tuple(Tuple({})))

for which the compiler currently outputs as type

((Unit *: EmptyTuple) *: EmptyTuple) *: EmptyTuple

instead of simply

(((Unit)))

(Don’t ask me whether such a type makes sense, but it could be written with such syntax.)

Literal syntax could exactly match type syntax also for zero and one element tuples.

(One could maybe even use the “new” Unit literal in type syntax? But not sure about that as this would end up in making Unit redundant. Maybe it would be good to also get rid of it, but maybe that’s too much of a change. Doing that would potentially loose a pronounceable name for this entity, which is not good.)

I’m aware that the idea to disallow parenthesis for grouping is bold. This is not only a diversion from what other programming language do but also from common math notation. Even people new to programming have some expectation that a computer can do math and understands math notation, as a computer is “a big calculator”.

Not being able to write a familiar

(21 + (1 + 2) * (3 + 4)): 42

expression, but being forced to write

{21 + {1 + 2} * {3 + 4}}: 42

is indeed questionable.

But math syntax is heavily overloaded. The expressions aren’t typed, so it makes no difference there.

Programming languages have different requirements. Writing code is not doing math!

Also the change comes with some ergonomic improvements:

Imagine you need to debug the above expression. You want to know what the first inner grouped expression (1 + 2) evaluates to.

When using braces you can simply place the cursor before the 1, press enter, and write some debugging code like

{21 + {
	val r = 1 + 2
	println(r)
	r} * {3 + 4}}//: 42

(We need to comment out the result type as the compiler is “to stupid” to remember that 1 + 2 is 3 and not only some arbitrary Int; but that’s a different story.)

Had you instead used parens for grouping you would need to first change them to braces, just to start debugging… That’s annoying! That’s bad for the same reason for why it was bad in Scala 2 to need to add braces when extending an one line method to a multi-line method, which was common for exactly such println debugging. That’s thankfully not necessary any more with the indentation syntax.

Of course “println debugging” here is just a placeholder for arbitrary refactorings.

To summarize:

The result of this move here would be super clean tuple syntax for all tuple arities, and no confusion with Unit literal syntax.

Using braces for grouping is actually more ergonomic than using parens, even it looks “a little bit funny” at first.

After thinking about it I came to the conclusion that {} as Unit literal is more consequential than a syntax that looks like an empty tuple. “Effectively nothing-really” is simply not a tuple—and you really don’t want to pass Unit by accident to a function that takes a tuple, which is almost certainly a bug. But nothing-really “grouped” into an expressions makes very much sens as Unit literal.

Oh, and one more thing: This would remove one case of “you can do that in different ways in Scala”. Currently you can use () and {} to group expressions, and when empty as Unit literal (to be honest, the later to my surprise). But why do we need so much flexibility in writing nothing-really? Why do we need to explain the difference between () and {} for grouping? Actually for no reason as braces always work (in contrast to parens).

No of the proposed syntax changes introduces new syntax (besides introducing the missing Tuple0 and Tuple1 literals of course). At the same time it would streamline and align the current syntax options a lot. That’s simpler to teach, and simpler to remember.

The only change of the status quo would be that now you’re forced to use grouping braces instead of just having the option to do so. Less syntax options, less choice paralysis. Less arguing about “style”… That’s very beginner friendly!

And for the friends of esthetics and symmetry:

((( {} ))) == ( {} ) // Compile error!
// The symbol pattern looks different, so
// how can it be the same?
// You simply can't compare
// `Tuple(Tuple(Tuple({}))) == Tuple({})`
// Makes no sense.

If everything were arbitrary, yes, that would work. The relearning penalty on that one is really steep, though; no way it’s worth it.

3 Likes

I just found out that there is even precedence in the compiler for the move to curly braces to group expressions.

var k = "23"
val nt = (k = "v")

This will result in a warning:

Deprecated syntax: in the future it would be interpreted as a named tuple with one element, not as an assignment.

To assign a value, use curly braces: {key = "value"}.
This can be rewritten automatically under -rewrite -source 3.6-migration.

Currently it’s also confusing that deleting the name in an one element named tuple will end up as just the value, instead of the value still wrapped in a tuple:

val nt1: Tuple = (k = "v") // Fine
val nt2: Tuple = (/*k = */"v") // Error
// Found: ("v" : String), Required: Tuple

The above proposal would also solve such confusion and irregularity.

Also that proposal would avoid to have different syntax for named and unnamed tuples of one element. (Alternatively, named tuple syntax needs further adjustments.)