Syntax for type tuple with one element

For now, if we want to write a type tuple from one element, we should use
MyType *: EmptyTuple
or Tuple1[MyType] because (MyType) is interpreted as MyType.

Can we better handle such cases?
For example, we can allow trailing commas in tuples, and (MyType,) will be the short synonym
of Tuple1[MyType].

3 Likes

At least we should allow the trailing comma for a one-element tuple: that’s already how it prints out!

scala> val t = Tuple1(3)
val t: Tuple1[Int] = (3,)
3 Likes

I agree, that would be the right tweak. My main concern is about the effort this will require. Changing the parser is the least of our worries. We also need to change scalameta/metals and IntelliJ, write a SIP, discuss it and get it through. All worth doing, but someone would have to take the lead on this.

1 Like

I actually hate this feature in Python, it’s so easy to accidentally forget the comma while refactoring. That being said, I think it’s a better fit for Scala due to the compile time verification.

1 Like

I think this is a great idea, the syntax is a but funny but there are no better options so the python folks ended up with the same solution

the only other sort of alternative is an implicit conversion from T => Tuple1[T], but i think its still nice to have a direct literal suntax (even if its a bit awkward) so people can write out Tuple1 literals directly without going through complicated conversions

One tangential concern is we need to make sure it plays well with trailing commas, which Scala already supports in limited scenarios, some of which may overlap with the scenarios we want to use Tuple1 literals

4 Likes

This is the first time I knew there exits a Tuple1!

3 Likes

Perhaps, no new syntax is necessary whatsoever.

Currently there are tuple constructors in the object Tuple already, e.g.:

scala> Tuple()
val res0: EmptyTuple.type = ()
                                                                                                                                                                                                          
scala> Tuple("one")
val res1: String *: EmptyTuple = (one,)

A sad thing though is that it only works for arities 0 and 1:

scala> Tuple("one", "two")
-- [E134] Type Error: ----------------------------------------------------------
1 |Tuple("one", "two")
  |^^^^^
  |None of the overloaded alternatives of method apply in object Tuple with types
  | [T](x: T): T *: EmptyTuple
  | (): EmptyTuple
  |match arguments (("one" : String), ("two" : String))
  |
  |where:    T is a type variable
1 error found

Perhaps, if Scala could get Tuple#apply methods for other arities up to 22 then Tuple( <arg-list> ) could become a mainstream general purpose way to construct tuples in place.

Moreover, it would line-up with the List syntax in a good way (if we think about tuples as heterogenous lists), i.e.:

1 :: 2 :: 3 :: Nil <=> List(1, 2, 3)

likewise

1 *: "2" *: 3.45 *: EmptyTuple <=> Tuple(1, "2", 3.45)

(if there was Tuple#apply for 3 args, apparently).

In other words, extending the standard library by adding more apply to object Tuple may look simpler and less controversial (and more intuitive) way rather than modifying Scala syntax itself (along with all the corresponding downstream stuff) just to support 1 particular case for arity 1.

Unless the goal is to provide the extreme level of conciseness, of course, saving the whole 4 characters of typing:

Tuple(1)
(1,) // 4 chars shorter

But in the latter case there’s still a question on how to support empty tuples, because Tuple() “just works” but () is not a tuple either. Would Scala consider adding support for (,) syntax too?

2 Likes

The original post was about type tuple, not tuple. i.e. Tuple1[Type1] but … it was error Tuple1[A] is not a subtype of NonEmptyTuple. …
Ohh, it’s another problem: Tuple1, \dots TupleN are subtypes of Product, not Tuple
Tuple itself consists from :* and EmptyTuple only (scala3/library/src/scala/Tuple.scala at 3.6.2 · scala/scala3 · GitHub )
If we work on the type level, we have no Tuple1[..], … TupleN[..]. Sorry for the mess.

(after checking: Tuple1[A] <: NonEmptyTuple, but this is made with compiler magic, not library code). … and IntelliJ does not know about this ;).

1 Like

Indeed, and I think this is also something that beginners could find difficult because of its non-orthogonality. In fact, any time you have learned a syntax rule of a new language, the exceptions are the ones causing the headaches. Given you know, for example, that a sequence of numbers between brackets constitute a Tuple, and most instantiations can be done via the class name, these are highly irregular:

val t0 = Tuple()  // () is unit
val t1 = Tuple(1) // (1) is not a Tuple
val t2 = (1,2)    // Tuple(1,2) not allowed

Welcome to Scala!

If we start accepting a trailing comma (which looks horrible to me) we might enter the same issues:

val t0 = (,)    // newcomer: what is this??
val t1 = (1,)   // fair, it's not just a number in brackets ...
val t2 = (1,2)  // (1,2,) not allowed??

A possibility to make this simpler and more orthogonal would be to define the Tuple by double brackets:

val t0 = (())    // Empty Tuple, or use Tuple() which should work as well.
val t1 = ((1))   // Tuple with one element, or use Tuple(1) which should work as well. 
val t2 = ((1,2)) // Tuple with two elements, or use Tuple(1,2) which should work as well'

etc, and while we are at it use

val u: Unit = unit // drop the (), its immediately clear the unit is an instance of Unit (and only 2 chars more)

I am not saying we should change this for that would cause other issues, but rather that we should keep orthogonality more in (on top of) mind when we are making changes to the language.

2 Likes

Emphasizing that point:

This one actually took me by surprise when it came up upthread. I haven’t had a chance to do much Scala3 yet (and hence, haven’t had an opportunity to play with Tuples being rewritten as essentially HLists), but I had always assumed that Unit was essentially a synonym for EmptyTuple.

Both syntactically and semantically that makes at least intuitive sense (indeed, I’ve sometimes taught Unit more or less that way); I can believe that there are deeper language reasons why it can’t be so, but I agree that it’s surprising.

We had EmptyTuple = Unit originally, but this ran into issues. Maybe others can chime in what the issues were, my memory is a bit hazy here.

About Tuple(...) constructors which work for arity 0 and 1 but not beyond: I think we are simply missing a vararg version of Tuple. It would be good to file an issue so that it’s not forgotten.

2 Likes

EmptyTuple = Unit was a no-go for Scala.js. In Scala.js, Unit must be JavaScript’s undefined value. We could not reconcile that with the fact that EmptyTuple needs to extend scala.Product.

2 Likes

If we had type Tuple1[T] = T then (value) would already be a tuple. Is the thing preventing unwrapped values from being interpreted as tuples of arity 1 that tuples need to inherit from Product? Perhaps this could be designed to work with type-classes or implicit conversions. I guess the latter would be a source-compatible solution.

in general its not good to have magic flexible types, and single parens are important for disambiguating operator precedence, and would be useless to box into a tuple.

It depends on how you look at it I guess. The way I’d like to see it, a tuple of one element is the element, so (1) just means 1, like it always has. That it’s considered a tuple where it needs to be is a type-level detail.

1 Like

There were languages that tried this idea (that an element is the automatically convertible to a singleton sequence): APL and XML for instance. But the widespread consensus is that it brings more problems than benefits.

That’s good to know. Was it general conversion to sequences or analog to a definition of implicit def toTuple[T](t: T): Tuple1(t)?. As tuples are generally less common in user-code I suspect there wouldn’t be many collisions as broader T -> Iterable[T] but of course my gut feeling might be wrong. On the plus side it would solve the asymmetry of functions like

def f(t: Tuple) = t

f(1,2) // aside: I really like the auto-tupling here.
f(3)
1 Like

Both versions are similarly fast to type, I think.

In the first case you would type t, select the right code suggestion (witch should be directly Tuple(@) in context), and than type 1. [@ is cursor]

In the second case you need to type a parenthesis, 1 and , . Typing the opening paren usually needs the SHIFT key and is a little harder to type than t.

Both is three keystrokes, as I see it. (In case code suggestion isn’t intelligent the first variant would be maybe 4, max 5 keystrokes. But simpler to type letters than the parenthesis.)

Were any of the languages that tried that strongly typed, and triggered the conversion only when the expected target type is a tuple?

That would be the case when using a Scala 3 Conversion.

I should play around with the idea. It’s cheap to test as it can be implemented in user-space. The point is: I think it wouldn’t be problematic at all. Scala 3’s Conversions behave quite reasonable.

Besides the missing varargs constructor I think it would be nice and consistent if v could be treated as (v,), just without the need for this quite ugly “comma hack”. (I was quite surprised by this syntax when I encountered it in Python the first time; I did not understand it intuitively and thought it’s a typo; I wasn’t new to programming at this point)

I think this looks really convincing!

Why can’t this be somehow special cased for Scala.js?

Because EmptyTuple = Unit + Conversion[T, Tuple1[T]] + the Tuple vararg constructor would make everything perfectly consistent and aligned.

val t0 = ()
val t1 = (1)
val t2 = (1,2)
val tn = …

would be (in some specific context) equivalent to

val t0 = Tuple()
val t1 = Tuple(1)
val t2 = Tuple(1,2)
val tn = …

Such symmetries are what makes a language easy to learn.

Special cases are OTOH really terrible.

Please replace “somehow” by something constructive, and then we can discuss. :wink: