Let's drop auto-tupling

Adriaan sent in scala/scala#6505, which drops semantic changing -Y flags. This reignited an old debate about auto-tupling. It seems to be that most people who have given some thought about this issue are coming down to drop this feature, instead of supporting it.

  • scalac, mind your own tuple business by Paul Phillips
  • seriously, given previous experience (i.e. man hours wasted) I’d rather have -Ywarn-adapted-args on by default at error level.
    26 Feb 2013 Roland Kuhn

  • AGGGHGHGHGHGHGHGHGHGHGHGHGH!
    Scala auto-tupling. Why on earth does this “feature” even exist?
    4 Feb 2014 Daniel Spiewak

  • Yes, let’s all vote to have this removed. I wonder if there’s anyone on planet who ever found it useful.
    4 Feb 2014 Rahul Goma Phulore

  • Not classifying as a spec bug, because I believe we should instead deprecate this.
    SI-3583: Spec doesn’t mention automatic tupling, Adriaan Moors

and on scala/scala#6505 comments are coming in from various users:

tpolecat 2 hours ago

I find that auto-tupling hinders readability, and at least for me it ends up being applied accidentally often enough that I prefer to turn it off. The 22-element limit makes it unusable for arity-abstraction so I don’t really buy that argument. So I’m :-1:for removing the flag and :+1:for removing the feature.

sjrd an hour ago

One data point: a very nasty issue (scala-js/scala-js#3154, fix in scala-js/scala-js@9428b68) that could have been avoided if auto-tupling did not exist.

So yup, I’m not a fan of auto-tupling …

Without building consensus around this for both Scala 2.x and Dotty, I don’t think we can move forward, so here it is.

what is auto tupling?

scala> case class W[A](elem: A)(implicit ordering: Ordering[A])
defined class W

scala> W(1, 2, 3, 4)
res0: W[(Int, Int, Int, Int)] = W((1,2,3,4))

In the above example, Scala automatically turns the parameter list 1, 2, 3, 4 into a tuple (1, 2, 3, 4) even though one might not expect it to happen.

A case that bites you is when a Java library exposes a method with Any. Using Paul’s example, Joda time 1.6:

libraryDependencies += "joda-time" % "joda-time" % "1.6"

Given this, can you spot the problem here?

scala> import org.joda.time.DateTime
import org.joda.time.DateTime

scala> def time = new DateTime(2010, 5, 7, 0, 0, 0)
time: org.joda.time.DateTime

scala> time
java.lang.IllegalArgumentException: No instant converter found for type: scala.Tuple6
  at org.joda.time.convert.ConverterManager.getInstantConverter(ConverterManager.java:165)
  at org.joda.time.base.BaseDateTime.<init>(BaseDateTime.java:169)
  at org.joda.time.DateTime.<init>(DateTime.java:168)
  at .time(<console>:12)
  ... 36 elided

Joda time 1.6 DateTime has 12 constructors, and among them were:

DateTime(int year, int monthOfYear, int dayOfMonth, int hourOfDay, int minuteOfHour, int secondOfMinute, int millisOfSecond) 

DateTime(Object instant) 

The first one takes seven parameters, but you passed only six. And Scala turned it into Tuple6. wat? I mean…

This is a DEBACLE!

17 Likes

Where’s the poll? I’m for removing auto-tupling.

I’ve seen auto-tupling in action mostly when my workmates written \/-() instead of \/-(()) or \/-(1, 2) instead of \/-((1, 2)). They thought the code is okay, so then they ignored warnings generated by compiler, but confusion remained.

It already warns. But there are what I consider false positives. An example from today:

import Ordering.Implicits._

(h, m) <= (21, 0)

Arguably, infix notation should not allow multiple arguments. But it does, so you can write:

List(1, 2, 3, 4) slice (1, 3)

I would argue that that looks like tuple notation more than method argument parentheses. But scalac doesn’t see it that way, apparently.

There are other use cases that I run into often enough.

1 Like

Do you mean -Ywarn-adapted-args? A warning that tells “this may not be what you want” I don’t think should exist. It should just not compile.

Basically anything that’s currently using auto-tupling would need more parens, if we dropped it. It’s an interesting example too because as you suggest, the second set of parens visually looks likes tuple literal, but it’s actually just method application parens.

Would the language be more consistent if we had Tupple.apply, similar to List.apply?

scala> List(1, 2) <= List(21, 0)
res7: Boolean = true

scala> Tuple(1, 2) <= Tuple(21, 0)

Auto tupling seems more important when you use a space instead of a dot when calling methods. Maybe it can be made context sensitive?

An example is when adding values to a map.

x += (1, 2)
becomes
x.+=((1,2))
whereas
x.+=(1,2) wouldn’t be helpful to have auto tuple, and I wouldn’t mind an error.

Same comment as above. Tuple(1, 2) would clear it up. To put in an example, what should the following return?

scala> class Y {
         def +=[A](a: A): Int = 0
         def +=(t: Tuple2[Int, Int]) = 1
         def +=[A, B, C](a: A, b: B, c: C = 0): Int = 2
       }
defined class Y

scala> (new Y) += (1, 2)

Similar thread in old scala-debate.
The Magnet Pattern uses this feature, though I don’t know how many people use this pattern in real world.

1 Like

The drawbacks of anywhere-autotupling are documented in this thread but it would be a shame to lose what is effectively our only tool for safe mixed-typed variadics. It seems like if autotupling is going to mostly go away, it’d be nice to get some way to get it back via a request at the definition site.

1 Like

Why not use varargs?

Unfortunately, varargs can’t preserve the types of the individual items. Even if you use a generic, everything gets upcast to the least upper bound. The trio of generics, autotupling, and typeclasses allow you to actually write safe arity-generic methods (within any relevant tuple size limits).

There is the hacky pattern to get around this by implicit-converting everything to a “bundle” type of the actual argument and the corresponding typeclass instance, but it feels really icky.

2 Likes

Thanks for explaining. Yes, this use case would not be supported, but I’d argue it would be more useful to have direct HList support in the language (granted, that’s a hypothetical feature right now, but one I’d very much like to implement).

5 Likes

Oh for sure! Language-integrated HLists would be great! But aren’t the reworked tuples for Dotty basically HLists? And I still want to be able to write a method where the user calls me as foo(4, "x") but I get an HList like (4, "x").

If we are talking hypotheticals, what if there were a phantom typeclass scala.reflect.ParamList[_], so only if you have:

def foo[A: ParamList](params: A): Unit = ???

The compiler would pass in a tuple? Basically make it an opt-in thing.

4 Likes

Perhaps you could auto-tuple only when the upper bound of the parameter is a subtype of Product (since there is no Tuple trait… or alternatively introduce trait Tuple extends Product and make tuples subtypes of Tuple). When HLists ever make it to Scala, that would become HList instead of Product/Tuple.

It seems like the hypothetical new tuples in dotty have this Tuple trait, which is actually equivalent to HList.

4 Likes

At first you scared me with reflect but yes, if would reliably get a tuple type in A, that would actually be better than how it works now since autotupling doesn’t happen for single arguments presently. It passes muster for capability but I’m not sure that it would pass muster for “language integration”: we seem to mostly use symbolic sigils to alter argument-passing semantics (=> and *).

By the way, would (potential) dropping of auto-tupling contradict with dotty-way of having two-argument function passing to a thing expecting a tuple? I mean, as far as I remember it is supported (or planned to be supported) in dotty to be able to do this:

def apply2ArgToTupled[A, B, C](s: Seq[(A, B)], f: (A, B) => C): Seq[C] = s map f

instead of old-fashioned current

def apply2ArgToTupled[A, B, C](s: Seq[(A, B)], f: (A, B) => C): Seq[C] = s map { case (a, b) => f(a, b) }

I understand that from compiler perspective this case looks like just a syntax sugar for partial functions in one particular case, but in fact it is an auto-tupling which is even recently added (to dotty).

I’m in favor of that kind of tupling, but @joroKr21 pointed out that they also have the tupling that’s more like varags for method calls, except that you don’t explicitly request it. Any 1-argument method where a TupleN is a valid argument type (i.e., it’s polymorphic or takes an Any), can receive either that 1 argument, or N of them, boxed into a tuple. I haven’t verified this, but that’s my understanding.

I just turned auto-tupling off in the Dotty build and regression tests and got about 30 failing tests. I’ll try next to restrict auto-tupling to only the case where the expected type is already a tuple type. Let’s see what that that gives.

The reverse conversion mentioned by @buzden is called auto-untupling. It is completely independent.

3 Likes

So I experimented some more. The result is in https://github.com/lampepfl/dotty/pull/4311. After having done the experiment I came back with mixed feelings. We definitely have to make an exception for infix operators. In retrospect, it looks like the current rules in Dotty (which are stricter than scalac’s) are already sufficient to address the confusing situations with auto-tupling that were mentioned in this thread. In particular the Joda-time example would not auto-tuple and give an error. In light of this I am not sure we need to go further.

1 Like

Thank you for considering the feedback! I am optimistic that we can find a good path forward.