Skip to content

Instantly share code, notes, and snippets.

@getify
Last active June 2, 2021 15:41
Show Gist options
  • Save getify/c8f017cc45246c723af7fd1b0b6af496 to your computer and use it in GitHub Desktop.
Save getify/c8f017cc45246c723af7fd1b0b6af496 to your computer and use it in GitHub Desktop.
Proposal: curried function declarations in javascript -- aka, making FP development in JS much much nicer
// 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; }
@getify
Copy link
Author

getify commented Oct 22, 2017

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment