-
-
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); // 6Same 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:undefinedSo, 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.