2

There are a few examples in this functional programing book about equivalency between different functions. For example, if my understanding is correct it means:

func(param => otherFunc(param))
func(otherFunc)                   // simplified

And, if I can articulate it in words to the best of my ability, I would say:

A function that takes a certain number of parameters and then only returns a second function called with those parameters as arguments is the same as a function that just takes the second function as a single parameter.

Is this a correct understanding? What would be some examples that would show how this would work? The only ones I can think of myself so far are quite trivial, so I'd like to see if I can see more examples to deepen my understanding of some applications of this.

David542
  • 104,438
  • 178
  • 489
  • 842
  • 1
    I discuss this concept [in this answer of mine](https://stackoverflow.com/a/59878788). The context is a bit more concrete (using this kind of reduction in `.then()`) but the explanation should cover a lot of of the generic case. – VLAZ Jun 23 '22 at 16:58
  • Doesn't this mean that func = otherFunc ? – IT goldman Jun 23 '22 at 17:01
  • @ITgoldman no, it means `param => otherFunc(param)` is the same as `otherFunc` – VLAZ Jun 23 '22 at 17:03
  • You have to be careful when doing these transformations in JavaScript. In some cases `func()` passes extra arguments that usually get ignored, and `otherFunc()` has additional optional arguments. When you put these two things together, you get unexpected results. See https://stackoverflow.com/questions/262427/why-does-parseint-yield-nan-with-arraymap – Barmar Jun 23 '22 at 17:12
  • 2
    Your example doesn't "return a second function". It actually calls the second function and returns the result. This is equivalent to just the second function. You have to be very careful with terminology when discussing first-class functions. – Barmar Jun 23 '22 at 17:15
  • @Barmar yes, the equality holds if the arity is the same in both cases. Otherwise, there is not guarantee that it would work the same. Chances are it wouldn't. – VLAZ Jun 23 '22 at 17:15
  • This question might be more appropriate for [cs.se]. – Barmar Jun 23 '22 at 17:17
  • 3
    I'm really surprised that the book doesn't give the name of this technique (which would allow you to research the topic better). It's [η-reduction](https://en.wikipedia.org/wiki/Eta_reduction). – Bergi Jun 23 '22 at 19:27
  • @Bergi would you want to show an example of how the eta reduction works on the above javascript example? – David542 Jun 26 '22 at 05:55
  • 1
    @David542 the example *is* eta reduction. It's when you convert `x => f(x)` into just `f` to peel off the useless wrapper function. – VLAZ Jun 26 '22 at 06:45
  • 1
    @David542 Not sure what you mean. You've already shown an example yourself? – Bergi Jun 26 '22 at 06:45
  • @Bergi sure but how would my javascript example translate into the more mathematical notation used in lambda calculus for the eta reduction? that's the part I don't understand. – David542 Jun 27 '22 at 22:21
  • 1
    @David542 `func(otherFunc)` is function application, in lambda calculus often `func otherFunc` (but that doesn't make it easier for newcomers). And the JS `func(param => otherFunc(param))` can be written in lambda calculus as `func (λparam . otherFunc param)`. The point being that `λparam . otherFunc param` = `otherFunc`. – Bergi Jun 27 '22 at 23:02

5 Answers5

2

In the lambda calculus, which is the foundation for a lot of functional programming concepts, this is called Eta reduction and is one of the few fundamental reduction operations in the system.

η-Reduction
LC: λx.f x = f
Javascript: (x => f(x)) = f

It is quite trivial to prove to yourself that this works. "Wrapping" the function in a lambda just explicitly provides the next argument which is applied anyway.

For the sake of brevity assume we have an increment function INC and LC numbers in the church encoding.
LC: (λx.INC x) 3 -> INC 3 -> 4 = INC 3 -> 4
Javascript: (x => INC(x))(3) -> INC(3) -> 4 = INC(3) -> 4

This breaks down a little bit if you have a multi-argument function. Since pure lambda calculus only has unary functions, eta reduction is this simple, but in JS we can use a trick to capture any number of arguments.

JS "Eta" Equvilancy
((...x) => f(...x)) = f

As for applications, you can use this fact to simplify expressions. Other than that there isn't really a reason to expand expressions this way, only reducing them for sake of performance and readability.

Yes, these are equivalent, and this is actually a core functional programming principle that you have discovered.

Bergi
  • 630,263
  • 148
  • 957
  • 1,375
Eric Breyer
  • 720
  • 1
  • 10
  • could you explain a bit more about how this part is reduced: `λx.fx = f`. I'm sure it's "trivial", but could you just show a step-by-step with the lambda calculus? – David542 Jun 28 '22 at 03:37
  • 1
    lambda-calculus has three reduction operations. \ -conversion - `λx.fx = λy.fy`, β-reduction (application) - `(λx.fx)2 = f2` , η-reduction - `λx.fx = f`. So it's not so much a consequence of other steps as it is a "step" itself. However, by applying beta reduction you can see that eta reduction holds true. `(λx.INCx)3 -> INC3 -> 4 = INC3 -> 4`. Further reading: https://en.wikipedia.org/wiki/Lambda_calculus#reduction – Eric Breyer Jun 28 '22 at 14:18
  • 3
    Just to expand a bit on what @EricBreyer said: alpha reduction is simply renaming. In terms of JS that is like doing `f = x => x + 1` to `f = num => num + 1`. They are equivalent, only the parameter name is changed. Beta reduction would be inlining the parameter with the value for it. If you have `(num => num + 1)(2)` that calls the function with the value `2` which can be re-written to `2 + 1` by substituting `num`. And eta reduction is just removing wrapper functions: `x => isNaN(x)` to `isNaN`. Beta reduction can show it clearly `(x => isNaN(x))(anyValue)` is same as `isNaN(anyValue)` . – VLAZ Jun 28 '22 at 15:59
  • Of course there is a reason to expand expressions that way, which is to delay their evaluation until the actual application, turning `f(x)` into `(y => f(x)(y))`. – Will Ness Jul 03 '22 at 01:59
2

I'll try to do my best explanation by step by step examples.

Step 1

Function definition

In javacript you can define a function with 2 different syntax

function inc(number) {
  return number + 1
}

or

const inc = (number) {
  return number + 1
}

// or simplifed

const inc = (number) => number + 1

Those 2 syntax are more or less equivalent but the second one is stays that you have a constant called inc (so a variable) that have a pointer to a piece of memory that contains a function.

So, inc is a variable that contains (as value) the function payload.

Step 2

This step is not related with the previous one

Supposing that we have an array and we want to increment each element into a new array we can use the function map.

Map is a function that creates a new array populated with the results of calling a provided function on every element in the calling array.

Mozilla Reference

const result = [1,2,3,4].map((number) => number + 1)
console.log(result)

Step 3

Let's make ingredients together

We have have a inc function and we want to use that function in our map.

We can do a very simple things.

const inc = (x) => x + 1

const result = [1,2,3,4].map(number => inc (number))
console.log(result)

Step 4

Let's try another approach.

Forget for a moment what we have done on step 3, trying using our inc function. We will try another approac.

At step 1 we have seen that a function is defined as follows.

const inc = (number) => number + 1

The semantics around the equal sign is quite powerful and is saying:

the expression inc

is equivalent to

the expression (number) => number + 1

We can take our inc function definition and the Step 2 code.

const inc = (number) => number + 1
const result = [1,2,3,4].map((number) => number + 1)

On the right side of both lines we have a duplicated expression:

(number) => number + 1

And first line says that an equivalent expression is inc. Trying to apply this approach the results is:

const inc = (x) => x + 1
const result = [1,2,3,4].map(inc)
console.log(result)

when you are trying to read a code written in this way you can imagine to inline inc label with its own expression.

Bonus

Let's apply the approach with more than 1 parameter

Trying to apply this approach for 2 parameter function we need another usecase. Given an array of numbers we want the sum of each number.

To achieve that we'll use the standard reduce function.

Mozilla Reference

const result = [1,2,3,4].reduce((a, x) => a + x, 0)
console.log(result)

now we can extract a sum function and replace the expression with it's label.

const sum = (a, b) => a + b
const result = [1,2,3,4].reduce(sum, 0)
console.log(result)
Claudio
  • 3,060
  • 10
  • 17
1

These two

func(param => otherFunc(param))
func(otherFunc)

are not exactly the same, the reason being that these two are not exactly the same

f1 = param => otherFunc(param)
f2 = otherFunc

Indeed look at this example:

// silly function taking two args and producing a string
function f(x,y) {
    return "<" + x + ',' + y + ">";
}

f1 = f;
f2 = x => f(x);

console.log(f1(2,3)); // ok
console.log(f2(2,3)); // not ok!

The reason they are not the same is that x => f(x) is a function that only takes 1 argument, even though the function f expects 2, so when you feed f2 with 2 arguments, it ingnores the second argument, and then calls f with 1 argument only, which results in the second argument (of f) being undefined.

Clearly, you could have written f2 as (x, y) => f(x, y), but then you'd be in trouble if f was ternary.

So, if you don't want to spell out all the arguments of the wrapper function to make it agree with f, you have to write the wrapper function like this

f2correct = (...x) => f(...x);

which expresses the idea that f2correct can take any number of arguments, and it forwards them all to f. In fact, f2correct is equivalent to f.

As regards your understanding of that, I think you got close, but probably misworded it.

A function that takes a certain number of parameters and then only returns a second function called with those parameters as arguments is the same as a function that just takes the second function as a single parameter the second function.

Enlico
  • 23,259
  • 6
  • 48
  • 102
  • 1
    Eta reduction leads to the same result when the arity is the same, e.g. `(x) => f(x)` for `f` being unary and `(x, y) => g(x, y)` for `g` being binary. Or also if the function is treated as the same arity as the wrapper: `Promise.resolve("3.14").then(x => parseInt(x))` is the same as doing `.then(parseInt)` while the infamous `["1.14", "2.14", "3.14"].map(parseInt)` isn't the same as `.map(x => parseInt(x))`. So yes, while in the most general case removing the wrapper function is not the same, in *a lot* of cases, eta reduction does apply. Helps that FP doesn't tend to use variadic functions. – VLAZ Jun 26 '22 at 08:26
  • @VLAZ, I've better clarfied that my suggestion of using `(...x) => f(...x)` was aiming at the general that you don't know how many arguments `f` takes. But thanks for the comment. As regards FP tending not to use variadic functions, Haskell doesn't indeed, but Clojure does have and, as far as I've understood, use them exensively. – Enlico Jun 26 '22 at 08:39
1

It is probably "easier" (YMMV) to see when you evaluate expressions.

Let's start with something simple:

42 === (40 + 1 + 1); // true
42 === (40 + 2);     // true
42 === (42);         // true
42 === 42;           // true

Let's define inc and evaluate it:

const inc = x => x + 1;

inc === (inc); // true
inc === inc;   // true

And so:

inc(41) === (inc)(41); // true
inc(41) === inc(41);   // true

Important bit is here: (inc).

The expression evaluates the function and simply returns it. So when you do (inc)(41) you end up doing inc(41).

Let's inline inc in the right operand:

inc(41) === (x => x + 1)(41); // true
//           ^^^^^^^^^^
//           i.e. inc

Now let's add an unnecessary wrapper:

   inc(41) === (n => (x => x + 1)(n))(41); // true
//              ^     ^           ^   ^^
//              |     |           |   |
//              |     +<<<<<<<<<<<+   |
//              |                 |   |
//              +>>>>>>>>>>>>>>>>>+   |
//              |                     |
//              +<<<<<<<<<<<<<<<<<<<<<+

Let's simplify a little bit:

   inc(41) === (n => inc(n))(41); // true
//              ^        ^   ^^
//              |        |   |
//              |        |   |
//              +>>>>>>>>+   |
//              |            |
//              +<<<<<<<<<<<<+

Or just:

inc(41) === (inc)(41); // true

It also helps to know how to read things:

(n => inc(n))(41)

The expression n => inc(n) (which is a function) is applied to 41. (At this point n is equal to 41.) Then inc is applied to n.

So you might as well just apply inc to 41 directly and skip the first expression, i.e.:

(n => inc(n))(41) === inc(41); // true
customcommander
  • 17,580
  • 5
  • 58
  • 84
  • 1
    It's not clear what the intended difference between `inc` and `(inc)` is (or between `42` and `(42)`) – Bergi Jun 27 '22 at 22:58
  • There's none. In my initial example I've used them to reduce expressions and show that they are equal notations by having them both standing on each side of a `===` operator. – customcommander Jun 28 '22 at 06:09
0

In JavaScript if you don't use parenthesis, you just get back a function (pointer).

Look at the difference between.

var a = func(x);
var b = func;

The first call executes the function func and returns the result and saves it into a.

The second call (without the parenthesis) returns the function as-is, and just binds it to b. After this, doing a.

var c = b(x)

should be the same as a, as long there are no side-effects.

Now look at the lambda-function.

var d = param => func(param);

Here you just create a function d that calls func and passes it arguments along to func.

You btw. also can write it this way.

var d = function(param) { return func(param) }

and this piece of code can be re-written.

function d(param) {
    return func(param);
}

as you now maybe see. d is the same as func. And again to the loop, this piece of code can be re-written.

var d = func;

Both function are the same.

That's why you can pass otherFunc in your example directly. You just pass the function as-is, without the need to create another new function.

David Raab
  • 4,433
  • 22
  • 40