June 09, 2014

Methods are not Functions

In Scala anyway.

Ok this was going to be a short post that I could link to when people get confused on #scala, but it turns out that there are a lot of cases to consider. It would be nice if this could just be the last word on methods vs. functions in Scala, so if I missed anything please let me know.

TL;DR … Methods in Scala are not values, but functions are. You can construct a function that delegates to a method via η-expansion (triggered by the trailing underscore thingy).

The definition I will be using here is that a method is something defined with def and a value is something you can assign to a val.

The Basic Idea

When we define a method we see that we cannot assign it to a val.

scala> def add1(n: Int): Int = n + 1
add1: (n: Int)Int

scala> val f = add1
<console>:8: error: missing arguments for method add1;
follow this method with `_' if you want to treat it as a partially applied function
       val f = add1
               ^

Note also the type of add1, which doesn’t look normal; you can’t declare a variable of type (n: Int)Int. Methods are not values.

However, by adding the η-expansion postfix operator (η is pronounced “eta”), we can turn the method into a function value. Note the type of f.

scala> val f = add1 _
f: Int => Int = <function1>

scala> f(3)
res0: Int = 4

The effect of _ is to perform the equivalent of the following: we construct a Function1 instance that delegates to our method.

scala> val g = new Function1[Int, Int] { def apply(n: Int): Int = add1(n) }
g: Int => Int = <function1>

scala> g(3)
res18: Int = 4

So that’s really all there is to it. The rest of this post is just details.

Automatic Expansion

In contexts where the compiler expects a function type, the desired expansion is inferred and the underscore is not needed:

scala> List(1,2,3).map(add1 _)
res5: List[Int] = List(2, 3, 4)

scala> List(1,2,3).map(add1)
res6: List[Int] = List(2, 3, 4)

This applies in any position where a function type is expected, as is the case with a declared or ascribed type:

scala> val z = add1
<console>:8: error: missing arguments for method add1;
follow this method with `_' if you want to treat it as a partially applied function
       val z = add1
               ^

scala> val z: Int => Int = add1
z: Int => Int = <function1>

scala> val z = add1 : Int => Int
z: Int => Int = <function1>

Effect of Overloading

In the presence of overloading you must provide enough type information to disambiguate:

scala> "foo".substring _
<console>:8: error: ambiguous reference to overloaded definition,
both method substring in class String of type (x$1: Int, x$2: Int)String
and  method substring in class String of type (x$1: Int)String
match expected type ?
              "foo".substring _
                    ^

scala> "foo".substring _ : (Int => String)
res14: Int => String = <function1>

Fistful of Parameters

Scala actually has a lot of ways to specify parameters, but they all work with η-expansion. Let’s look at each case.

Parameterless Methods

Methods with no parameter list follow the same pattern, but in this case the compiler can’t tell us about the missing _ because the invocation is legal on its own.

scala> def x = println("hi")
x: Unit

scala> val z = x // ok
hi
z: Unit = ()

scala> val z = x _
z: () => Unit = <function0>

scala> z()
hi

Note that unlike the method (which has no parameter list) the function value has an empty parameter list.

Multiple Parameters

Methods with multiple parameters expand to equivalent multi-parameter functions:

scala> def plus(a: Int, b: Int): Int = a + b
plus: (a: Int, b: Int)Int

scala> plus _
res8: (Int, Int) => Int = <function2>

Methods with multiple parameter lists become curried functions:

scala> def plus(a: Int)(b: Int): Int = a + b
plus: (a: Int)(b: Int)Int

scala> plus _
res11: Int => (Int => Int) = <function1>

Perhaps surprisingly, such methods also need explicit η-expansion when partially applied:

scala> plus(1)
<console>:9: error: missing arguments for method plus;
follow this method with `_' if you want to treat it as a partially applied function
              plus(1)
                  ^

scala> plus(1) _
res13: Int => Int = <function1>

However curried functions do not.

scala> val x = plus _
x: Int => (Int => Int) = <function1>

scala> x(1) // no underscore needed
res0: Int => Int = <function1>

Type Parameters

Values in scala cannot have type parameters; when η-expanding a parameterized method all type arguments must be specified (or they will be inferred as non-useful types):

scala> def id[A](a:A):A = a
id: [A](a: A)A

scala> val x = id _
x: Nothing => Nothing = <function1>

scala> val y = id[Int] _
y: Int => Int = <function1>

scala> y(10)
res2: Int = 10

Implicit Parameters

Implicit parameters are passed at the point of expansion and do not appear in the type of the constructed function value:

scala> def foo[N:Numeric](n:N):N = n
foo: [N](n: N)(implicit evidence$1: Numeric[N])N

scala> foo[String] _
<console>:9: error: could not find implicit value for evidence parameter of type Numeric[String]
              foo[String] _
                 ^

scala> foo[Int] _
res3: Int => Int = <function1>

scala> def bar[N](n:N)(implicit ev: Numeric[N]):N = n
bar: [N](n: N)(implicit ev: Numeric[N])N

scala> bar[Int] _
res4: Int => Int = <function1>

By-Name Parameters

The “by-nameness” of by-name parameters is preserved on expansion:

scala> def foo(a: => Unit): Int = 42
foo: (a: => Unit)Int

scala> foo(println("hi"))
res15: Int = 42

scala> val x = foo _
x: (=> Unit) => Int = <function1>

scala> x(println("hi"))
res16: Int = 42

Also note that η-expansion can capture a by-name argument and delay its evaluation:

scala> def foo(a: => Unit): () => Unit = a _
foo: (a: => Unit)() => Unit

scala> val z = foo(println("hi"))
z: () => Unit = <function0>

scala> z()
hi

Sequence Parameters

Sequence (“vararg”) parameters become Seq parameters on expansion:

scala> def foo(as: Int*): Int = as.sum
foo: (as: Int*)Int

scala> def x = foo _
x: Seq[Int] => Int

scala> x(1,2,3)
<console>:10: error: too many arguments for method apply: (v1: Seq[Int])Int in trait Function1
              x(1,2,3)
               ^

scala> x(Seq(1,2,3))
res2: Int = 6

Default Arguments

Default arguments are ignored for the purposes of η-expansion; it is not possible to use named arguments to simulate partial application.

scala> def foo(n: Int = 3, s: String) = s * n
foo: (n: Int, s: String)String

scala> foo _
res19: (Int, String) => String = <function2>

scala> foo(42) _
<console>:9: error: not enough arguments for method foo: (n: Int, s: String)String.
Unspecified value parameter s.
              foo(42) _
                 ^

Is that all?

I think so, but let me know if I missed one. There are a lot of cases but no real surprises.

Can’t think of anything else to say about this, sorry.