4

I love currying but there are a couple of reasons why a lof of Javascript devs reject this technique:

  1. aesthetic concerns about the typical curry pattern: f(x) (y) (z)
  2. concerns about performance penalties due to the increased number of function calls
  3. concerns about debugging issues because of the many nested anonymous functions
  4. concerns about readability of point-free style (currying in connection with composition)

Is there an approach that can mitigate these concerns so that my coworkers don't hate me?

  • 1
    You forgot testing. Testing simple functions is easy, testing complicated functions is hard. Testing a pipeline of simple functions is still easy, testing the combinatorial explosion of interactions of monolithic functions is still hard(er). – Jared Smith Feb 10 '17 at 16:49
  • 1
    Really cool question. We often think of our code in a bubble, but the reality is often times it lives out in the wild where many other people interact with it. It's good to have awareness of this and think about the improvements we can make that helps out everyone in the long run. – Mulan Feb 10 '17 at 17:03

2 Answers2

2

Note: @ftor answered his/her own question. This is a direct companion to that answer.

You're already a genius

I think you might've re-imagined the partial function – at least, in part!

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

and it's counter-part, partialRight

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

partial takes a function, some args (xs), and always returns a function that takes some more args (ys), then applies f to (...xs, ...ys)


Initial remarks

The context of this question is set in how currying and composition can play nice with a large user base of coders. My remarks will be in the same context

  • just because a function may return a function does not mean that it is curried – tacking on a _ to signify that a function is waiting for more args is confusing. Recall that currying (or partial function application) abstracts arity, so we never know when a function call will result in the value of a computation or another function waiting to be called.

  • curry should not flip arguments; that is going to cause some serious wtf moments for your fellow coder

  • if we're going to create a wrapper for reduce, the reduceRight wrapper should be consistent – eg, your foldl uses f(acc, x, i) but your foldr uses f(x, acc, i) – this will cause a lot of pain amongst coworkers that aren't familiar with these choices

For the next section, I'm going to replace your composable with partial, remove _-suffixes, and fix the foldr wrapper


Composable functions

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

const comp = (f, g) => x => f(g(x));

const foldl = (f, acc, xs) => xs.reduce(f, acc);

const drop = (xs, n) => xs.slice(n);

const add = (x, y) => x + y;

const sum = partial(foldl, add, 0);

const dropAndSum = comp(sum, partialRight(drop, 1));

console.log(
  dropAndSum([1,2,3,4]) // 9
);

Programmatic solution

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

// restore consistent interface
const foldr = (f, acc, xs) => xs.reduceRight(f, acc);

const comp = (f,g) => x => f(g(x));

// added this for later
const flip = f => (x,y) => f(y,x);

const I = x => x;

const inc = x => x + 1;

const compn = partial(foldr, flip(comp), I);

const inc3 = compn([inc, inc, inc]);

console.log(
  inc3(0) // 3
);

A more serious task

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

const filter = (f, xs) => xs.filter(f);

const comp2 = (f, g, x, y) => f(g(x, y));

const len = xs => xs.length;

const odd = x => x % 2 === 1;

const countWhere = f => partial(comp2, len, filter, f);

const countWhereOdd = countWhere(odd);

console.log(
   countWhereOdd([1,2,3,4,5]) // 3
);

Partial power !

partial can actually be applied as many times as needed

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)

const p = (a,b,c,d,e,f) => a + b + c + d + e + f

let f = partial(p,1,2)
let g = partial(f,3,4)
let h = partial(g,5,6)

console.log(p(1,2,3,4,5,6)) // 21
console.log(f(3,4,5,6))     // 21
console.log(g(5,6))         // 21
console.log(h())            // 21

This makes it an indispensable tool for working with variadic functions, too

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys)

const add = (x,y) => x + y

const p = (...xs) => xs.reduce(add, 0)

let f = partial(p,1,1,1,1)
let g = partial(f,2,2,2,2)
let h = partial(g,3,3,3,3)

console.log(h(4,4,4,4))
// 1 + 1 + 1 + 1 +
// 2 + 2 + 2 + 2 +
// 3 + 3 + 3 + 3 +
// 4 + 4 + 4 + 4 => 40

Lastly, a demonstration of partialRight

const partial = (f, ...xs) => (...ys) => f(...xs, ...ys);

const partialRight = (f, ...xs) => (...ys) => f(...ys, ...xs);

const p = (...xs) => console.log(...xs)

const f = partialRight(p, 7, 8, 9);
const g = partial(f, 1, 2, 3);
const h = partial(g, 4, 5, 6);

p(1, 2, 3, 4, 5, 6, 7, 8, 9) // 1 2 3 4 5 6 7 8 9
f(1, 2, 3, 4, 5, 6)          // 1 2 3 4 5 6 7 8 9
g(4, 5, 6)                   // 1 2 3 4 5 6 7 8 9
h()                          // 1 2 3 4 5 6 7 8 9

Summary

OK, so partial is pretty much a drop in replacement for composable but also tackles some additional corner cases. Let's see how this bangs up against your initial list

  1. aesthetic concerns: avoids f (x) (y) (z)
  2. performance: unsure, but i suspect performance is about the same
  3. debugging: still an issue because partial creates new functions
  4. readability: i think readability here is pretty good, actually. partial is flexible enough to remove points in many cases

I agree with you that there's no replacement for fully curried functions. I personally found it easy to adopt the new style once I stopped being judgmental about the "ugliness" of the syntax – it's just different and people don't like different.

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Thanks for your response, it is inspiring. I need to think more about partial application. I've completely ignored it so far. AFAIK `acc` in `reduceRight` should be appended from the right. Apparently Javascript's `reduceRight` prepends it. That's weird and yields weird results for non-commutative operations. If `curry` don't flip arguments, a function like `drop` would have to be defined as `(n, xs) => xs.slice(n)`, which were kind of uncommon. –  Feb 10 '17 at 20:40
  • If you're creating wrappers, I suppose it's up to you how they behave. As for the `drop` argument order, I can see compelling arguments for both cases; however, if the order cannot be channged from `drop = (xs,n) => ...`, `partialRight` could be useful here – `const partialRight = (f,...xs) => (...ys) => f(...ys, ...xs)` – then `partialRight(drop, 1) ([1,2,3]) // => [2,3]` – Mulan Feb 10 '17 at 21:27
  • I updated the code leaving your `drop` in tact and demonstrated using `partialRight` instead. I provided another `partialRight` example later in the answer too. Good to see you on SO again, btw ^_^ – Mulan Feb 10 '17 at 21:40
0

The current prevailing approach provides that each multi argument function is wrapped in a dynamic curry function. While this helps with concern #1, it leaves the remaining ones untouched. Here is an alternative approach.

Composable functions

A composable function is curried only in its last argument. To distinguish them from normal multi argument functions, I name them with a trailing underscore (naming is hard).

const comp_ = (f, g) => x => f(g(x)); // composable function

const foldl_ = (f, acc) => xs => xs.reduce((acc, x, i) => f(acc, x, i), acc);

const curry = f => y => x => f(x, y); // fully curried function

const drop = (xs, n) => xs.slice(n); // normal, multi argument function

const add = (x, y) => x + y;

const sum = foldl_(add, 0);

const dropAndSum = comp_(sum, curry(drop) (1));

console.log(
  dropAndSum([1,2,3,4]) // 9
);

With the exception of drop, dropAndSum consists exclusively of multi argument or composable functions and yet we've achieved the same expressiveness as with fully curried functions - at least with this example.

You can see that each composable function expects either uncurried or other composable functions as arguments. This will increase speed especially for iterative function applications. However, this is also restrictive as soon as the result of a composable function is a function again. Look into the countWhere example below for more information.

Programmatic solution

Instead of defining composable functions manually we can easily implement a programmatic solution:

// generic functions

const composable = f => (...args) => x => f(...args, x);

const foldr = (f, acc, xs) =>
 xs.reduceRight((acc, x, i) => f(x, acc, i), acc);

const comp_ = (f, g) => x => f(g(x));

const I = x => x;

const inc = x => x + 1;


// derived functions

const foldr_ = composable(foldr);

const compn_ = foldr_(comp_, I);

const inc3 = compn_([inc, inc, inc]);


// and run...

console.log(
  inc3(0) // 3
);

Operator functions vs. higher order functions

Maybe you noticed that curry (form the first example) swaps arguments, while composable does not. curry is meant to be applied to operator functions like drop or sub only, which have a different argument order in curried and uncurried form respectively. An operator function is any function that expects only non-functional arguments. In this sence...

const I = x => x;
const eq = (x, y) => x === y; // are operator functions

// whereas

const A = (f, x) => f(x);
const U = f => f(f); // are not operator but a higher order functions

Higher order functions (HOFs) don't need swapped arguments but you will regularly encounter them with arities higher than two, hence the composbale function is useful.

HOFs are one of the most awesome tools in functional programming. They abstract from function application. This is the reason why we use them all the time.

A more serious task

We can solve more complex tasks as well:

// generic functions

const composable = f => (...args) => x => f(...args, x);

const filter = (f, xs) => xs.filter(f);

const comp2 = (f, g, x, y) => f(g(x, y));

const len = xs => xs.length;

const odd = x => x % 2 === 1;


// compositions

const countWhere_ = f => composable(comp2) (len, filter, f); // (A)

const countWhereOdd = countWhere_(odd);

// and run...

console.log(
   countWhereOdd([1,2,3,4,5]) // 3
);

Please note that in line A we were forced to pass f explicitly. This is one of the drawbacks of composable against curried functions: Sometimes we need to pass the data explicitly. However, if you dislike point-free style, this is actually an advantage.

Conclusion

Making functions composable mitigates the following concerns:

  1. aesthetic concerns (less frequent use of the curry pattern f(x) (y) (z)
  2. performance penalties (far fewer function calls)

However, point #4 (readability) is only slightly improved (less point-free style) and point #3 (debugging) not at all.

While I am convinced that a fully curried approach is superior to the one presented here, I think composable higher order functions are worth thinking about. Just use them as long as you or your coworkers don't feel comfortable with proper currying.