Toc
  1. 维基百科定义
  2. MDN
  3. Currying in JS
    1. The Strategy
    2. Curry Wrapper
    3. Recursive Curry
    4. Tweaks and Optimisations
    5. Variadic Curry
    6. Bonus
Toc
0 results found
catzillaorz
闭包为什么叫闭包
维基百科定义

在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是在支持头等函数的编程语言中实现词法绑定的一种技术。闭包在实现上是一个结构体,它存储了一个函数(通常是其入口地址)和一个关联的环境(相当于一个符号查找表)。环境里是若干对符号和值的对应关系,它既要包括约束变量(该函数内部绑定的符号),也要包括自由变量(在函数外部定义但在函数内被引用),有些函数也可能没有自由变量。闭包跟函数最大的不同在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即便脱离了捕捉时的上下文,它也能照常运行。捕捉时对于值的处理可以是值拷贝,也可以是名称引用,这通常由语言设计者决定,也可能由用户自行指定(如C++)。

MDN

A closure is the combination of a function and the lexical environment within which that function was declared.— MDN

Currying in JS

We can’t write the JS engine to currify all functions, but we can come up with a strategy to do so.

The Strategy

Examining the two forms of sum3 reveals an important fact. The actual function body from the plain function gets transferred as-is to the last function in the chain.

function sum3(x, y, z) { //... }
function sum3(x) { return (y) => { return (z) => { // ... }; }; }

Before we reach the last level, we won’t have all the arguments in the execution scope. This means that we create a wrapper function that collects the arguments and calls the real function. All the intermediate nested functions are called accumulator functions — at least that’s what I call them.

function _sum3(x, y, z) { return x + y + z; }
function sum3(x) { return (y) => { return (z) => { return _sum3(x, y, z); }; }; }
sum3(1)(2)(3) // 6 <-- It works!
Curry Wrapper

Since we’re using a wrapper function in place of the real function, we can create another function that generates this wrapper function. We’ll call this new function curry — a higher-order function.

function curry(fn) { return (x) => { return (y) => { return (z) => { return fn(x, y, z); }; }; }; }
const sum3 = curry((x, y, z) => { return x + y + z; });
sum3(1)(2)(3) // 6 <-- It works!

All we now need are different arity curry functions that take: 0 arguments, 1 argument, 2 arguments and so on — not really.

Recursive Curry

Instead of writing multiple wrapper functions, we will write a curry function such that it works for functions of all arity.

Let’s say we do write multiple curry functions. This is what they’ll look like:

function curry0(fn) { return fn(); }
function curry1(fn) { return (a1) => { return fn(a1); }; }
function curry2(fn) { return (a1) => { return (a2) => { return fn(a1, a2); }; }; }
function curry3(fn) { return (a1) => { return (a2) => { return (a3) => { return fn(a1, a2, a3); }; }; }; }
...
function curryN(fn){ return (a1) => { return (a2) => { ... return (aN) => { // N-th nested function return fn(a1, a2, ... aN); }; }; }; }

We can make a few observations:

  • The i-th accumulator returns another function that is the (i+1)-th accumulator. We can also say that it is the j-th accumulator.
  • The i-th accumulator accepts the i-th argument, while maintaining the previous i - 1 arguments in its environment.
  • There will be N nested functions, where N is the arity of function fn.
  • The N-th function always calls and returns fn.
  • curry0 is pointless waste of a function and should not exist.

Using the above observations, we can see that the curry function returns a nested structure of self-similar accumulator functions. We can easily generate such a structure using recursion.

function nest(fn) { return (x) => { // accumulator function }; }
function curry(fn) { return nest(fn); }

We obviously need a terminal case else our code will form an infinite fractal. We will maintain a variable i that keeps track of our current level. The terminal case will be when i === N.

function nest(fn, i) { return (x) => { if (i === fn.length) { return fn(); }
return nest(fn, i + 1); }; }
function curry(fn) { return nest(fn, 1); }

Next up, we need to store the accumulated arguments and pass them to fn() in the terminal case. The easiest solution is to create an array args inside curry and pass it to nest.

function nest(fn, i, args) { return (x) => { args.push(x);
if (i === fn.length) { return fn(...args); }
return nest(fn, i + 1, args); }; }
function curry(fn) { const args = []; return nest(fn, 1, args); }

Just add a guard for 0-arity functions and we are done.

function curry(fn) { if (fn.length === 0) { return fn; }
const args = []; return nest(fn, 1, args); }

It’s a good idea to test our code at this stage:

const log1 = curry((x) => console.log(x)); log1(10); // 10
const log2 = curry((x, y) => console.log(x, y)); log2(10)(20); // 10 20

You can run your tests here on codepen.

Tweaks and Optimisations

At this point, we can either do nothing or we can try to optimise our code.

For starters, we can move nest inside curry, thus reducing the number of arguments passed to nest by reading fn and args from the closure.

function curry(fn) { if (fn.length === 0) { return fn; }
const args = [];
function nest(i) { return (x) => { args.push(x);
if (i === fn.length) { return fn(...args); }
return nest(i + 1); }; }
return nest(1); }

Let’s tweak this new curry to be more functional and not rely on closure variables. We do this by supplying args and fn.length as nest arguments.

Further still, instead of passing fn.length for comparison, we can pass the remaining depth of recursion.

function curry(fn) { if (fn.length === 0) { return fn; }
function nest(N, args) { return (x) => { if (N - 1 === 0) { return fn(...args, x); }
return nest(N - 1, [...args, x]); }; }
return nest(fn.length, []); }
Variadic Curry

Let’s see our old friend sum3 and its older brother sum5:

const sum3 = curry((x, y, z) => { return x + y + z; });
const sum5 = curry((a, b, c, d, e) => { return a + b + c + d + e; });
sum3(1)(2)(3) // 6 <-- It works! sum5(1)(2)(3)(4)(5) // 15 <-- It works!

Look at this abomination of a code! No doubt it works, but it looks …

In haskell and other functional languages, the syntax is pretty relaxed. Compared to this monstrosity, let’s see how haskell deals with it:

let sum3 x y z = x + y + z let sum5 a b c d e = a + b + c + d + e
sum3 1 2 3 > 6
sum5 1 2 3 4 5 > 15
sum5 1 2 3 (sum3 1 2 3) 5 > 17

If you ask me, even the simple function calls in JS look better:

sum5(1, 2, 3, 4, 5) // 15

But this doesn’t mean we have to give up on currying. What we can do is settle for some middle ground. A system where the curried, the uncurried syntaxes both work and the ones in between work too.

sum3(1, 2, 3) sum3(1, 2)(3) sum3(1)(2, 3) sum3(1)(2)(3)

We need a simple modification — convert accumulators to variadic functions.

However, when the i-th accumulator accepts k arguments, the next accumulator won’t be of N - 1 depth, but rather N - k depth. We were using N - 1 because all accumulators were accepting one argument each.

This also means that we no longer require the 0-arity guard.

function curry(fn) { function nest(N, args) { return (...xs) => { if (N - xs.length === 0) { return fn(...args, ...xs); }
return nest(N - xs.length, [...args, ...xs]); }; }
return nest(fn.length, []); }

It’s testing time. You can run your tests here on codepen.

function curry(){...}
const sum3 = curry((x, y, z) => x + y + z);
console.log( sum3(1, 2, 3), sum3(1, 2)(3), sum3(1)(2, 3), sum3(1)(2)(3), ); // 6 6 6 6
Bonus

We did it! We created a curry function that takes multi-arity functions and returns variadic curried function(s). This would be a good time to take a bow.

There is yet another method of currying in JS.

In JS, we can bind arguments to a function and create a bound copy of it. The resultant function is said to be “partially applied” — partially, because the function already holds some of its arguments, but requires some more before invocation.

Up until now, curry would return a function that accumulates arguments until all arguments were received and then invoke fn with the arguments. By binding arguments to the function, we can remove the need for multiple nested accumulator functions.

And this is what we get:

function curry(fn) { return (...xs) => { if (xs.length >= fn.length) { return fn(...xs); }
return curry(fn.bind(null, ...xs)); }; }

Here’s how it works. curry takes an N-arity function and returns an accumulator function. When the accumulator is invoked with k number of arguments, we check if k >= N, i.e. whether or not the function’s arity is satisfied.

If it is, we invoke fn with the arguments. If not, we create a copy of fn that has k arguments partially applied and pass it to curry as the next fn, with its reduced arity of N - k.

20201227143812-2020-12-27-14-38-13-

打赏
支付宝
微信
本文作者:catzillaorz
版权声明:本文首发于catzillaorz的博客,转载请注明出处!