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; }
@benlesh
Copy link

benlesh commented Oct 22, 2017

👍

@kyleshevlin
Copy link

I like the syntax. Don't love the "loose currying". I would argue that if the function is curried, it's curried. Apply the arguments one at a time. That being said, I think there are a swath of people who would find this syntax much more legible than the arrow functions. I've worked with people that, for one reason or another, just don't grasp the series of arrow functions. I think this could help those people.

@ichpuchtli
Copy link

Beautiful solution.

@rickmed
Copy link

rickmed commented Oct 22, 2017

Yes! Not sure about the "loose currying" option though.

@getify
Copy link
Author

getify commented Oct 22, 2017

The "loose currying" is basically how almost every FP-in-JS library does currying. The spirit of currying (from Haskell) is one-at-a-time, which I would call "strict currying", but because of the syntactic overhead of (..) parentheses in JS, some people would rather do foo(1,2) if they have two inputs to pass at once, instead of foo(1)(2).

If we want people to prefer this syntax over a curry(..) method in a FP lib, I think we need to support the convenience patterns that are most common.

@gziolo
Copy link

gziolo commented Oct 22, 2017

That would be awesome 💯

@renatorib
Copy link

👍

@acutmore
Copy link

acutmore commented Oct 22, 2017

What happens when we throw rest args into the mix?

function f(...a)(...b){ }

Only allow rest args in last block? This may complicate parsers as function f(...a) is a valid series of tokens but function f(...a)( is not.

Default args seem to hold up okay:

function f(a = 1)(b) { };
f(undefined, 1);
f()(1);
f(2); // returns partially applied function

function g(a)(b = 2) { };
g(1, 1);
g(1); // returns partially applied function
g(1)();
g(1, undefined);

function h(a, b = 2)(c) { };
h(1, 2, 3);
h(1) === h(1, 2);
h(1)(3) === h(1, 2, 3);
h(1)(3) !== h(1, 3); // writers must be aware that not equivalent in this case

@getify
Copy link
Author

getify commented Oct 22, 2017

Only allow rest args in last block?

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.

@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