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.)

I totally agree that current state where (k = "v") is fine but (/*k = */"v") is no tuple anymore is very confusing. But I personally feel that changing grouping from parentheses to curly braces could be extremely invasive.

Perhaps, it would be better to take the opposite way – find out a way to improve tuple syntax on both types and value literals level.

There’s a big discussion in the other thread regarding collection literals, and I personally believe these two things are connected: if there were more unambiguous and unified way to define tuples and tuple literals, then such syntax could be used for collection literals as well, because tuples are collections. Copying my message from the other thread:

1 Like

Thinking about it, this is the only way I see that makes any sense, even if it looks weird
Especially (,) but we can’t use EmptyTuple = Unit, and if we infered which one it is depending on expected type, it wouldn’t solve the x == (x) != x *: EmptyTuple problem

One way to make it less weird is to discourage expressions in singleton tuples:

val isThisATuple = (1 + 5 * veryLongFunction(a, b, c)(using myGiven),)

For example through a default scalafix rule

On the other hand, I was surprised that (1, 2,) doesn’t work, since we allow these kinds of things in other places

In summary:

  • Allow trailing commas on tuples: (1, 2,) == (1, 2)
  • Allow creation of tuples of 0 and 1 element with (,) and (1,)

Bonus:

  • Deprecate () in favor of unit, allows us to show the user a warning/error to point in the right direction: “Deprecated, please use unit for the Unit literal or (,) for the EmptyTuple literal”
4 Likes

We might really want to allow () as a tuple literal:

val obviouslyATuple: Tuple = (
  // 1,
  // 2,
)
// error: Found: Unit, Required: Tuple

(Of course I’m not saying the compiler should read comments)

My instinct tells me there is extremely few () where the expected type is not Unit

Therefore another solution would be to:

  • If the expected type is Tuple, () is EmptyTuple

Bonuses:

  • Deprecate () when there is no expected type
  • Allow unit which has type Unit no matter the expected type

Of course it’s always better to have dedicated syntax for things like:

myList.foldLeft( () ){ x =>
  ???
}

In this case IIRC the type parameter of foldLeft will be determined depending on the “zero”, in this case ().
Therefore there will not be an expected type, so we will not infer EmptyTuple, even if the body of the fold is clearly talking about tuples.

2 Likes

Personally, I prefer those proposals that suggest to slightly modify the tuple syntax itself, rather than attempts to make the existing syntax working in cases where it doesn’t work now (for tuples of arities 0 and 1). From my prospective it would have the following PROS:

  • it wouldn’t change meaning of any existing code, i.e. no breaking changes would be introduced – the old syntax could continue working as usual. Instead it could be gradually deprecated and phased out later.
  • the modified tuple syntax would allow to make it less confusing in regards to grouping parentheses and such.

Personally I like the idea of introducing some special char in front of the opening parenthesis, e.g. #(, *(, @(, etc. – whatever works. It would allow to make it working on both type and value levels, e.g.:

type T = @(Int, String)
val t: T = @(1, "two")

(again, I have no personal preference, which character to use)

Also, such syntax looks like a case class but without class name, i.e. pretty functional and aligned with all other Scala syntax.

There’s a unit defined somewhere in Scala? I’ve had one in my teaching library for years because I hate the () literal, but I can get rid of it if there’s a standard one.

I understand that there are technical difficulties, but I would really prefer () to be an empty tuple and unit to be the Unit value (and get rid of that ML legacy once and for all).

5 Likes

In theory, {} does exist already and could be used in place of ().
Moreover, {} better resembles the idea of “empty” expression that doesn’t do or return anything. Therefore, Scala could use {} for unit literals and re-reserve () for empty tuples.

However, to make () as tuple literal really helpful, it should work in type definitions too, but currently it doesn’t:

type T = ()

compiler thinks it is a beginning of () => ... whatever ... and fails if it is not.

Besides, there’s still an issue with tuples of arity 1: syntax (a,) seems feasible but pretty clumsy because the trailing comma is only required for arity 1 (maybe 0, it depends), but not for arities > 1.

That said, I’m not trying to “push” any particular solution. But personally, I’d prefer to have some unified approach that would work on both type and literal levels and for all tuple arities starting with 0.

3 Likes

There is not, these points are proposed extensions of the language !

I don’t think we should introduce more symbolic literals to Scala, especially not for Unit, where unit would work perfectly well !

As for re-reserving existing syntax, that might be a really bad idea
It was @sjrd who was the first to explain me why, I think it goes something like this:

  • We cannot guarantee no one upgrades from version X to version Y, no matter how far away these are. Therefore people might upgrade from the version where (): Unit, no warning, to the one where (): EmptyTuple no warning
  • Similarly, we cannot guarantee all material will be updated, especially physical books, it would be extremely confusing for someone to follow a book and the syntax they learn does something completely different

In this case, this seems like a very small change, so maybe it would still be acceptable ? I don’t know

This could be added without (technical) issue today, as the syntax currently gives an error. On its own however, it would make it even more jarring that () has type Unit and not type () !

Totally agree, the only one I see realistically working is (...,) is always a tuple

The other desired one would be (...) is always a tuple, but I am very doubtful this can be done because of parenthesized expressions: x * (a + b) vs x *: (a + b)

1 Like

The reason it doesn’t work is because Tuple1[T] != T. Even so we can pretend this is the case when the type is known with implicit conversions. It’s not perfect, but then none of the alternatives seem to be either.

The unit value must be different from EmptyTuple, and therefore so must their types.

However, I can see something working to adapt a literal () to EmptyTuple if the expected type is <: Tuple. We could also consider introducing Predef.unit as a more explicit value of type Unit. Over time, if we can nudge the community to use unit and/or {} instead of () for the unit value, we might be able to teach () as being the empty tuple. And who knows, by the time I retire we might even deprecate () used as the unit value.

This is just my opinion on the technical feasibility.

10 Likes

No it wouldn’t. unit appears as a variable identifier both for units (meters, etc.) and for unit testing.

This is not one of the easier terms to reclaim. It can’t be a soft keyword; you have to be able to use it anywhere.

So your argument for not repurposing () applies to unit as well.

extension (u: Unit)
  inline def t: EmptyTuple = Tuple()

or somesuch would work.

1 Like

If it’s in Predef as proposed above, then it will just be shadowed without issue, no ?

I can see something working to adapt a literal () to EmptyTuple if the expected type is <: Tuple.

For me this seems like the most reasonable approach. It ties together nicely with 0-arity functions and also with () symbolising the empty return value

I do this with my kids, too. Probably the best we can do without breaking stuff is Unit.unit , then import Unit.unit , which is not so bad.

When I’ve needed my own type named Unit I use Vnit , which requires a short aside: “The category theory people got here first.”

That really offers nothing to the empty tupple problem. Maybe SomeTupleType.empty following the Seq.empty pattern is reasonable.

You could do something similar with an apply for a one-item tuple.

Pragmatically speaking - how useful is an empty or one-item tuple literal? Maybe importing those in special cases would be fine.

A common shadowed name is a pain, because when you see it you don’t know whether it’s the shadowed one or not.

In practice, people adopt patterns that avoid the collision, even if there are measures one can take (live with it, rename one, give the full namespace path, etc.).

Perhaps something simpler could be a solution such as just providing this in Predef (taking what @Ichoran suggested a step further):

extension [A](a: A) 
   def tup = a match 
     case _ : Unit => EmptyTuple
     case _  => Tuple1(a)