-
-
Save getify/c8f017cc45246c723af7fd1b0b6af496 to your computer and use it in GitHub Desktop.
// Standard: | |
function fn(x) { | |
return function(y){ | |
return function(z){ | |
return x * y / z; | |
}; | |
}; | |
} | |
// pros: function `fn()` is a regular function declaration, hoists | |
// cons: quite verbose, requires intermediate functions (often anonymous), | |
// can't do "loose currying" where you pass in more than one arg in a single | |
// call (like `fn(1,2)(3)`), JS engine probably can't inline/optimize away | |
// these intermediate functions as a result |
// Preferred: | |
var fn = x => y => z => x * y / z; | |
// pros: much shorter and thus a bit more visually attractive | |
// cons: doesn't hoist, all functions are anonymous (lexically)... | |
// even `fn()` -- it has the inferred name `fn`, but it's still | |
// lexically anonymous so it can't reliably self-reference itself, etc, | |
// some think having to essentially back-track or read right-to-left to | |
// parse arrow functions (parameters vs expression bodies) is less | |
// readable, same problems with "loose currying" and JS engine optimizations | |
// from previous snippet |
// Proposed: | |
function fn(x)(y)(z) { | |
return x * y / z; | |
} | |
// pros: real function declaration, still hoistable, would allow engine to | |
// support "loose currying" (like `fn(1,2)(3)`), and engine can more possibly | |
// optimize intermediate functions when they're not necessary, intermediate | |
// functions (when necessary) could have inferred names, like `fn@2` | |
// or `fn:2` or `fn#2`, sort of like bound methods (that have been partially | |
// applied), OR the "intermediate" function could actually just be the original | |
// `fn` and not a different one, but where JS is collecting its arguments, | |
// engine could maintain the `this` binding across all the function calls | |
// automatically | |
// cons: maybe a bit more complex of JS grammar for the parser, little more | |
// verbose in needing the (..)s to disambiguate syntax, intermediate functions | |
// still would be lexically anonymous |
// curried functions could still be expressions, even anonymous or arrows: | |
foo( function fn(x)(y)(z){ return x * y / z; } ); | |
foo( function(x)(y)(z){ return x * y / z; } ); | |
foo( (x)(y)(z) => x * y / z ); | |
// and still be concise methods/class methods/etc: | |
var o = { | |
fn(x)(y)(z) { return x * y / z; } | |
}; | |
// and still support multiple parameters at each level if you want: | |
function fn(x,y)(z) { return x * y / z; } |
Default args seem to hold up okay:
Some clarification around this: AFAICT, all implementations of curry in FP-in-JS libraries assume that if you explicitly say you are currying 3 arguments, it needs 3 explicit arguments, no skipping allowed. If you curry a function of arity 3 and two of them have "defaults", you can't just pass one explicit argument and expect curry to understand that and apply the defaults for the other two.
Furthermore, even if a curry(..)
could be that smart, that creates ambiguity, because... what if I don't want to invoke the default? How do I do that?
In other words, automatic defaults are incompatible with explicit-N currying in JS. That doesn't mean you can't do defaults, it means that to trigger a default, you have to pass an explicit undefined
in that argument position. Check this assertion against Ramda, lodash/fp, etc, and I think you'll see what I mean.
For example, Ramda's curryN(..)
explicit-arity currying:
function foo(x,y=2,z) { return x + y + z; }
var f1 = R.curryN(3,foo);
f1(1); // partially applied function
f1(1)(undefined); // partially applied function
f1(1)()(3); // partially applied function
f1(1)(undefined)(3); // 6
Same goes for lodash/fp's curryN(..)
.
But unfortunately Ramda's curry(..)
surprisingly just assumes whatever fn.length
reports, which is botched/unreliable with a function like foo(..)
above. According to spec, fn.length
stops counting in a parameter list once it encounters any non-simple parameter (like default, rest, etc).
function foo(x,y=2,z) { return x + y + z; }
foo.length; // 1 <-- oops!
var f1 = R.curry(foo);
f1(1); // NaN <-- x:1, y:2, z:undefined
So, should we copy/mimic the behavior more like R.curry(..)
or R.curryN(..)
?
I would say emphatically the former, not the latter. R.curry(..)
when used with functions with defaults can produce some really surprising/frustrating behavior. We should avoid anything surprising like that. R.curryN(..)
works really consistently.
The only way we could copy R.curry(..)
is if we insisted something very intrusive like, "if a parameter has a default, all other subsequent parameters also have to have a default." That would never fly, I don't think.
I would propose that JS's built-in curry would simply count parameter positions (not just the way fn.length
short-circuits), and infer that count as arity for currying. So foo(..)
above would be arity 3 (even though it has length
of 1).
Furthermore, you can't "skip" a parameter (like y
) in that case. You have to pass an explicit undefined
.
I feel strongly this is the least surprising (if slightly more onerous) thing that FP devs in JS would expect/understand.
Yes, only in last block (and possibly not even at all, if that makes the grammar too hard). They would be helpful in curried functions, but I wouldn't view them as strictly necessary for the kinds of FP usages of curry that are most common.