Why case pattern matching anonymous functions can have two signatures, and have magical call-site compatibility

Viewed 397

In Scala, the pattern matching anonymous function keeps surprising me. I just discovered the following behaviors, and I wanna know how to understand it.

It seems, the parameter signature of the anonymous function { case (a, b) => (b, a) } can be annotated both as a single tuple parameter and a tuple of parameters:

scala> ({ case (a, b) => (b, a) }: ((Int, Int)) => (Int, Int))
val res1: ((Int, Int)) => (Int, Int) = $Lambda$1160/0x0000000801127040@689fe2a3

scala> ({ case (a, b) => (b, a) }: (Int, Int) => (Int, Int))
val res2: (Int, Int) => (Int, Int) = $Lambda$1161/0x000000080106e840@1784d711

Notice res1 has a tuple parameter, while res2 has two parameters. Why is that? Is this behavior defined in language specs? (Sorry didn't check out yet, it seems too dense for me at the moment.)

Also, the res1 function magically accepts both a tuple argument and two arguments at call-site.

scala> res1((1,2))
val res3: (Int, Int) = (2,1)

scala> res1(1,2)
val res4: (Int, Int) = (2,1)

In comparison, res2 won't accept a tuple argment:

scala> res2((1,2))
           ^
       error: not enough arguments for method apply: (v1: Int, v2: Int): (Int, Int) in trait Function2.
       Unspecified value parameter v2.

What's going on here? Why res1 can change its signature as it sees fit?

3 Answers

Is this behaviour defined in language specs?

It does not seem to be specified according to open issue Spec doesn't mention automatic tupling #3583

What's going on here? Why res1 can change its signature as it sees fit?

This feature is called auto-tupling. It might make some call-sites more natural as discussed in Remove Two Warts: Auto-Tupling and Multi-Parameter Infix Operations #4311

(x, y) == z
z == ((x, y))    // !yuck

Many argue for its removal/restriction, for example, Let’s drop auto-tupling.

Compiler flag -Xlint:adapted-args can warn when auto tupling happens, for example

➜  ~ scala
Welcome to Scala 2.13.3 (OpenJDK 64-Bit Server VM, Java 1.8.0_265).
Type in expressions for evaluation. Or try :help.

scala> def foo (x: (Int, Int)) = x
def foo(x: (Int, Int)): (Int, Int)

scala> foo (1,2)
val res0: (Int, Int) = (1,2)

scala> :replay -Xlint:adapted-args
replay> def foo (x: (Int, Int)) = x
def foo(x: (Int, Int)): (Int, Int)

replay> foo (1,2)
        foo (1,2)
            ^
On line 2: warning: adapted the argument list to the expected 2-tuple: add additional parens instead
                signature: foo(x: (Int, Int)): (Int, Int)
          given arguments: 1, 2
         after adaptation: foo((1, 2): (Int, Int))
def foo(x: (Int, Int)): (Int, Int)
val res0: (Int, Int) = (1,2)

Another related flag is -Xlint:multiarg-infix, for example

➜  ~ scala
Welcome to Scala 2.13.3 (OpenJDK 64-Bit Server VM, Java 1.8.0_265).
Type in expressions for evaluation. Or try :help.

scala> def % (a: Int, b: Int) = (a,b)
def $percent(a: Int, b: Int): (Int, Int)

scala> :replay -Xlint:multiarg-infix
replay> def % (a: Int, b: Int) = (a,b)
            ^
        warning: multiarg infix syntax looks like a tuple and will be deprecated
def $percent(a: Int, b: Int): (Int, Int)

Scala will automatically convert an argument list to a tuple if required:

def f(arg: (Int, Int)) = ???

f(1, 2) // OK

It will not do the reverse conversion

def g(a: Int, b: Int) = ???

g((1, 2)) // Error "not enough arguments for method"

Needless to say there is some debate over this feature...

The other two answers are right that auto-tupling lets you pass in 2 arguments as if they were a tuple to res1, but they don't explain why res1 and res2 can have different types when they were created with the same function literal.

One nice thing about using pattern matching functions is that even when your function accepts multiple arguments, you can treat them as a single tuple and pattern match on that. (See Pattern Matching Anonymous Functions). Your type annotation tells the compiler that res2 has 2 Int parameters, so it basically turns your function literal into this (it tuples the arguments before pattern matching on them):

(arg1, arg2) => (arg1, arg2) match {
  case (a, b) => (b, a)
}

Whereas res1 is kept basically the same, since it accepts a single tuple input anyways:

tuple => tuple match {
  case (a, b) => (b, a)
}
Related