Skip to content

Commit

Permalink
improve fun-eta-expanion.md wording
Browse files Browse the repository at this point in the history
  • Loading branch information
bishabosha committed Jun 25, 2024
1 parent 005ae21 commit c7ef1d6
Showing 1 changed file with 59 additions and 43 deletions.
102 changes: 59 additions & 43 deletions _overviews/scala3-book/fun-eta-expansion.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
---
title: Eta Expansion
title: Eta-Expansion
type: section
description: This page discusses Eta Expansion, the Scala technology that automatically and transparently converts methods into functions.
description: This page discusses Eta-Expansion, the Scala technology that automatically and transparently converts methods into functions.
languages: [ru, zh-cn]
num: 31
previous-page: fun-function-variables
next-page: fun-hofs
---


When you look at the Scaladoc for the `map` method on Scala collections classes, you see that it’s defined to accept a _function_:
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:

{% tabs fun_1 %}
{% tab 'Scala 2 and 3' for=fun_1 %}

```scala
def map[B](f: (A) => B): List[B]
-----------
def map[B](f: A => B): List[B]
// ^^^^^^ function type from `A` to `B`
```

{% endtab %}
Expand All @@ -26,7 +26,7 @@ Indeed, the Scaladoc clearly states, “`f` is the _function_ to apply to each e
But despite that, somehow you can pass a _method_ into `map`, and it still works:

{% tabs fun_2 %}
{% tab 'Scala 2 and 3' for=fun_2 %}
{% tab 'Scala 2 and 3' %}

```scala
def times10(i: Int) = i * 10 // a method
Expand All @@ -36,80 +36,96 @@ List(1, 2, 3).map(times10) // List(10,20,30)
{% endtab %}
{% endtabs %}

Have you ever wondered how this works---how you can pass a _method_ into `map`, which expects a _function_?

The technology behind this is known as _Eta Expansion_.
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

{% comment %}
NOTE: I got the following “method” definition from this page (https://dotty.epfl.ch/docs/reference/changed-features/eta-expansion-spec.html), but I’m not sure it’s 100% accurate now that methods can exist outside of classes/traits/objects.
I’ve made a few changes to that description that I hope are more accurate and up to date.
{% endcomment %}

Historically, _methods_ have been a part of the definition of a class, although in Scala 3 you can now have methods outside of classes, such as [Toplevel definitions][toplevel] and [extension methods][extension].
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`).

Unlike methods, _functions_ are complete objects themselves, making them first-class entities.
_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.

Their syntax is also different.
This example shows how to define a method and a function that perform the same task, determining if the given integer is even:
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:

{% tabs fun_3 %}
{% tab 'Scala 2 and 3' for=fun_3 %}
{% tabs fun_2_expanded %}
{% tab 'Scala 2 and 3' %}

```scala
def isEvenMethod(i: Int) = i % 2 == 0 // a method
val isEvenFunction = (i: Int) => i % 2 == 0 // a function
def times10(i: Int) = i * 10
List(1, 2, 3).map(x => times10(x)) // eta expansion of `.map(times10)`
```

{% endtab %}
{% endtabs %}

The function truly is an object, so you can use it just like any other variable, such as putting it in a list:
> For the curious, the term eta-expansion has its origins in the [Lambda Calculus](https://en.wikipedia.org/wiki/Lambda_calculus).
## When does eta-expansion happen?

{% tabs fun_4 %}
{% tab 'Scala 2 and 3' for=fun_4 %}
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.)

{% tabs fun_5 class=tabs-scala-version %}
{% tab 'Scala 2' %}

In Scala 2 eta-expansion only occurs automatically when the expected type is a function type.
For example, the following will fail:
```scala
val functions = List(isEvenFunction)
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](#manual-eta-expansion) for how to solve this issue with manual eta-expansion.
{% endtab %}
{% endtabs %}

{% tabs fun_5 class=tabs-scala-version %}
{% tab 'Scala 2' for=fun_5 %}
{% tab 'Scala 3' %}

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.

```scala
// this example shows the Scala 2 error message
val methods = List(isEvenMethod)
^
error: missing argument list for method isEvenMethod
Unapplied methods are only converted to functions when a function type is expected.
You can make this conversion explicit by writing `isEvenMethod _` or `isEvenMethod(_)` instead of `isEvenMethod`.
```
def isLessThan(x: Int, y: Int): Boolean = x < y

Conversely, a method technically isn’t an object, so in Scala 2 you couldn’t put a method in a `List`, at least not directly, as shown in this example:
val methods = List(isLessThan) // works
```

{% endtab %}
{% endtabs %}

## Manual eta-expansion

{% tab 'Scala 3' for=fun_5 %}
You can always manually eta-expand a method to a function value, here are some examples how:

{% tabs fun_6 class=tabs-scala-version %}
{% tab 'Scala 2' %}

```scala
val functions = List(isEvenFunction) // works
val methods = List(isEvenMethod) // works
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
```

The important part for Scala 3 is that the Eta Expansion technology is improved, so now when you attempt to use a method as a variable, it just works---you don’t have to handle the manual conversion yourself.
{% endtab %}

{% tab 'Scala 3' %}

```scala
val methodsA = List(isLessThan(_, _)) // way 1: wildcard application
val methodsB = List((x, y) => isLessThan(x, y)) // way 2: anonymous function
```

{% endtab %}
{% endtabs %}

## Summary

For the purpose of this introductory book, the important things to know are:

- Eta Expansion is the Scala technology that lets you use methods just like functions
- The technology has been improved in Scala 3 to be almost completely seamless
- 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][eta_expansion] in the Reference documentation.

Expand Down

0 comments on commit c7ef1d6

Please sign in to comment.