When you look at the Scaladoc for the map
method on Scala collections classes, you see that it’s defined to accept a function value:
def map[B](f: A => B): List[B]
// ^^^^^^ function type from `A` to `B`
Indeed, the Scaladoc clearly states, “f
is the function to apply to each element.”
But despite that, somehow you can pass a method into map
, and it still works:
def times10(i: Int) = i * 10 // a method
List(1, 2, 3).map(times10) // List(10,20,30)
Why does this work? The process behind this is known as eta-expansion. It converts an expression of method type to an equivalent expression of function type, and it does so seamlessly and quietly.
The differences between methods and functions
The key difference between methods and functions is that a function is an object, i.e. it is an instance of a class, and in turn has its own methods (e.g. try f.apply
on a function f
).
Methods are not values that can be passed around, i.e. they can only be called via method application (e.g. foo(arg1, arg2, ...)
). Methods can be converted to a value by creating a function value that will call the method when supplied with the required arguments. This is known as eta-expansion.
More concretely: with automatic eta-expansion, the compiler automatically converts any method reference, without supplied arguments, to an equivalent anonymous function that will call the method. For example, the reference to times10
in the code above gets rewritten to x => times10(x)
, as seen here:
def times10(i: Int) = i * 10
List(1, 2, 3).map(x => times10(x)) // eta expansion of `.map(times10)`
For the curious, the term eta-expansion has its origins in the Lambda Calculus.
When does eta-expansion happen?
Automatic eta-expansion is a desugaring that is context-dependent (i.e. the expansion conditionally activates, depending on the surrounding code of the method reference.)
In Scala 2 eta-expansion only occurs automatically when the expected type is a function type. For example, the following will fail:
def isLessThan(x: Int, y: Int): Boolean = x < y
val methods = List(isLessThan)
// ^^^^^^^^^^
// error: missing argument list for method isLessThan
// Unapplied methods are only converted to functions when a function type is expected.
// You can make this conversion explicit by writing `isLessThan _` or `isLessThan(_,_)` instead of `isLessThan`.
See below for how to solve this issue with manual eta-expansion.
New to Scala 3, method references can be used everywhere as a value, they will be automatically converted to a function object with a matching type. e.g.
def isLessThan(x: Int, y: Int): Boolean = x < y
val methods = List(isLessThan) // works
Manual eta-expansion
You can always manually eta-expand a method to a function value, here are some examples how:
val methodsA = List(isLessThan _) // way 1: expand all parameters
val methodsB = List(isLessThan(_, _)) // way 2: wildcard application
val methodsC = List((x, y) => isLessThan(x, y)) // way 3: anonymous function
val methodsA = List(isLessThan(_, _)) // way 1: wildcard application
val methodsB = List((x, y) => isLessThan(x, y)) // way 2: anonymous function
Summary
For the purpose of this introductory book, the important things to know are:
- eta-expansion is a helpful desugaring that lets you use methods just like functions,
- the automatic eta-expansion been improved in Scala 3 to be almost completely seamless.
For more details on how this works, see the Eta Expansion page in the Reference documentation.