维基百科定义
在计算机科学中,闭包(英语: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.
|
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.
|
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.
|
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:
|
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.
|
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.
|
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.
|
Just add a guard for 0-arity functions and we are done.
|
It’s a good idea to test our code at this stage:
|
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.
|
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.
|
Variadic Curry
Let’s see our old friend sum3 and its older brother sum5:
|
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:
|
If you ask me, even the simple function calls in JS look better:
|
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.
|
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.
|
It’s testing time. You can run your tests here on codepen.
|
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:
|
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.