简体   繁体   中英

How to reconcile Javascript with currying and function composition

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?

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.

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM