Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve fun-eta-expanion.md wording #3034

Merged
merged 1 commit into from
Jun 25, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading