Multiarg infix application considered warty

There has been a thread about dropping autotupling and controlling infix but not specifically about autountupling the operand of an infix expression.

Autotupling is when x.op(y, z) is taken as x.op((y, z)).

Autountupling or multiarg infix is when x op (y, z) is taken as x.op(y, z) instead of x.op((y, z)).

It is specified in 6.12.3:

The right-hand operand of a left-associative operator may consist of several arguments enclosed in parentheses.

Here is an example of why it can be confusing. (The warning is inadvertently emitted in 2.13.3 without the lint flag; the behavior in 2.13.4 is shown below. The warning in 2.13.3 can be silenced with -Wconf:cat=lint-multiarg-infix:s.)

scala> val xs = collection.mutable.Map(1 -> 100)
val xs: scala.collection.mutable.Map[Int,Int] = HashMap(1 -> 100)

scala> xs += (2, 200)
              ^
       error: type mismatch;
        found   : Int(2)
        required: (Int, Int)
                 ^
       error: type mismatch;
        found   : Int(200)
        required: (Int, Int)

scala> :se -Xlint:multiarg-infix

scala> xs += (2, 200)
              ^
       error: type mismatch;
        found   : Int(2)
        required: (Int, Int)
                 ^
       error: type mismatch;
        found   : Int(200)
        required: (Int, Int)
          ^
       warning: multiarg infix syntax looks like a tuple and will be deprecated

scala> xs addOne (2, 200)
val res2: xs.type = HashMap(1 -> 100, 2 -> 200)

The purpose of the lint was to highlight the wart, and also as a chance to anticipate a possible change in Scala 3.

The purpose of this topic is to share information and encourage further discussion, especially about DSLs that leverage the syntax. Since “lints are made to be broken,” the lint can be selectively enabled. The ongoing discussion on the moribund Dotty PR can continue here. The current state seems to be that there are differing opinions about the language feature, how to improve it, and whether to improve it, but that usability remains a priority.

See the scala-dev ticket.

9 Likes

It seems like the use cases for multi-arg infix applications can be modeled using tuples, are there cases where this is not true?

If it does hold, and tuple usability is reasonable, it seems like this would bring us one step closer to being able to model all vararg methods this way. This would help ease some of the weirdness needed to build vararg methods which need each argument to be paired with a typeclass instance.

Examples of this are the string interpolators for cats.Show and anorm, which are complex enough that they regularly confuse IntelliJ’s import identifier. Not exactly a deal-breaker, but it’s a bit of a red flag that maybe this could be done better.

1 Like

Tuples tend to have a significant performance penalty. Thus, for anything performance-critical, tuple usability is not reasonable.

I’d be curious if this will still be the case, as at least the larger ones will be erased to an array-backed TupleXXL

No, that if anything will make things worse, since arrays have extra overhead. The hope is that the flexibility gained is more than worth the possible performance impact.

If you erase the tuple to multiple arguments, then you’d get somewhere.

I’m a little confused, varargs already come with the extra overhead of being packed into a WrappedArray, can you clarify why that would be less overhead than packing them into an array-backed TupleXXL?

Never mind, took me a second read through to get what you were saying, as most of the examples in the PR were for varargs functions like ScalaTest’s must contain (...).

I guess it comes down to what’s more valuable to the ecosystem, throwing away every DSL that depends on elegant multiarg infix syntax, vs. handling corner cases like the above.

// one of these is for humans, the other for the machine
def * = id ~ first ~ last <> (User.apply _, User.unapply _)

def * = (id ~ first ~ last).<>(User.apply _, User.unapply _)

1 Like

The runtime cost of tuples is, I think, a concern made trivial by the new inline facilities in Scala 3. Anyone wishing to avoid the runtime cost of tuples can inline them by forwarding to a non-tupled function:

object C {
  inline def +=(inline tuple: (Int, Int)) = inline tuple match { 
    case (a, b) => add(a, b) 
  }
  def add(a: Int, b: Int) = C
  
  C += (1, 2)
}

I wouldn’t argue that it’s feasible to do the same in Scala 2, as Scala 2 macros are too hardcore for newbies, but the new inlines are intuitive and fiddle-free and make it easy for any person to reduce the runtime cost of their DSLs as far as they’re willing to.

6 Likes

@kai - Yes, inline is awesome! However, this doesn’t help for as long as we need to maintain library compatibility with 2. After we can let that go, this solution is great.

1 Like

So… 5-10 years give or take…

Yes, but they are dog years.

1 Like

inline is awesome yes, but it’s not magic. A friend of mine has been trying and failing to avoid tuple allocation with inline. Doesn’t seem to quite work currently. This is what I get when I decompile the output of the snippet above.

import java.io.*;
import scala.*;
import scala.runtime.*;

public final class C$ implements Serializable
{
    public static final C$ MODULE$;
    
    static {
        new C$();
    }
    
    private C$() {
        MODULE$ = this;
        final Tuple2 apply = Tuple2$.MODULE$.apply((Object)BoxesRunTime.boxToInteger(1), (Object)BoxesRunTime.boxToInteger(2));
        final int a = 1;
        final int b = BoxesRunTime.unboxToInt(apply._2());
        this.add(1, b);
    }
    
    private Object writeReplace() {
        return new ModuleSerializationProxy((Class)C$.class);
    }
    
    public C$ add(final int a, final int b) {
        return this;
    }
}
1 Like

@Katrix - It ought to be magic that can handle this. Has your friend filed a bug report (or is there already a bug filed)?

2 Likes

No, I don’t think he came far enough to look at the decompiled output when he tested last time.

I think that it would suffice to enable local optimizations with the current (2.13) optimizer to eliminate this tuple and boxing and unboxing.