The other day somehow I ended reading this post and found an amazing piece of code:
const pipe = (...fns) => a => fns.reduce((b, f) => f(b), a);
Composing functions, that is, creating a function that chains several function calls passing the result of each function to the next... is nothing new, but the way this guy implements it using reduce quite caught my eye. Also, those 2 consecutive arrows puzzled me a bit. We have a function returning another function, but as both are written as arrows, the code seemed a bit strange to me at first... .
After this I was thinking about ways to compose functions provided by some commont javascript libraries. In lodash we have 2 options, _.chain and _.flow.
_.chain is not really a way to compose, but an alternative. When we want to compose functions provided by lodash, we can wrap our array/collection in a chainable object, and then chain method calls to that object. It's explained here.
let cities = [ "Paris", null, "Toulouse", "Xixon", "", "Berlin" ]; result = _.chain(cities) .compact() .take(3) .reverse() .value(); //result = [ 'Xixon', 'Toulouse', 'Paris' ]
Rather than _.chain we can use _.flow, like this:
result = _.flow([ _.compact, (ar) => _.take(ar, 3), _.reverse ])(cities);
Notice that with _.flow we are creating the composed function, that here we are invoking immediatelly, but that we could reuse for multiple calls. _.chain is just a one go approach. The other big difference is that _.chain is only valid when using lodash functions, while with _.flow we could be composing any sort of function.
The documentation of _.flow says something that at first read confused me a bit: "Creates a function that returns the result of invoking the given functions with the this binding of the created function". Well, when composing non-method functions most times they are not using "this" (as in the example above), so we don't care about it. But this is important when composing methods of a same object. Let's say I have this class:
class Formatter{ constructor(repetitions){ this.repetitions = repetitions; } format1(txt){ return `${"[" .repeat(this.repetitions)} ${txt} ${"]" .repeat(this.repetitions)}`; } format2(txt){ return `${"(" .repeat(this.repetitions)} ${txt} ${")" .repeat(this.repetitions)}`; } }
If we compose the calls to format1 and format2 like this we will obviously be missing "this" in the method, and the result will be wrong:
let formatter = new Formatter(2); let formatFn = _.flow([ formatter.format1, formatter.format2 ]); console.log(formatFn("hi")); // hi
With what we've read about "the binding of the created function", all we need to fix it is calling function.prototype.bind:
let boundFormatFn = formatFn.bind(formatter); console.log(boundFormatFn("hi")); // (( [[ hi ]] ))
If lodash were not providing this feature, we could just create additional functions to wrap each method call, or directly invoke bind on each method-function
let formatFn2 = _.flow([ (st) => formatter.format1(st), (st) => formatter.format2(st) ]); console.log(formatFn2("hi")); //(( [[ hi ]] )) console.log("------------------"); let formatFn3 = _.flow([ formatter.format1.bind(formatter), formatter.format2.bind(formatter) ]); console.log(formatFn3("hi")); //(( [[ hi ]] ))
I've uploaded all the code to this gist
By the way, if you want to read some javascript programming way more interesting than this post, you can check this article about currying that I found recently.
No comments:
Post a Comment