0

Is the lodash flow function a real compose function, or is it something that looks like one, but is optimized to run fast and sacrifices the flexibility I'd expect? I expected flow to return a function I could curry, but instead it gave back a function that uses Javascript's arguments keyword. So curry can't tell that there are pending arguments, and it just gets invoked immediately.

Working intuitively enough:

var add = function(x, y) { 
    return x + y 
};
var exclam = function(x) { 
    return x.toString() + "!"; 
}
exclam(1) // "1!"
add(1,2) // 3
var add1 = FP.curry(add)(1);
add1(4) // 5
var add1AndExclam = FP.flow([add1, exclam])
add1AndExclam(2) // "3!"

Non-intuitive result:

addAndExclam = FP.flow([add, exclam])
/*
function(){
    var t=arguments,e=t[0];
    if(i&&1==t.length&&yi(e)&&e.length>=200)return i.plant(e).value();
    for(var u=0,t=r?n[u].apply(this,t):e;++u<r;)t=n[u].call(this,t);
    return t
}
*/
addAndExclam(1,2) // "3!"
add1AndExclamV2 = FP.curry(addAndExclam)(1) // "NaN!"`

Is it overkill to look for another library to help with functional programming paradigms? Should I just whip up my own compose? I used lodash because it was already in my project. The documentation makes it look like flow should be lodash's compose.

I've also found it really difficult to curry the data argument in lodash's each (I wanted something like an eachMyArrayName shortcut). Whether I use curryRight or the lodash object placeholder convention.

Is lodash FP just for making lodash functions auto curriable? Or am I doing something wrong, and it is usable as the main functional programming helper?

Edit:

If I want to I can wrap the function like this, but it seems to defeat the purpose of meta programming to have boilerplate looking code.

add1AndExclamV2 = FP.curry(function(x, y) { 
    return addAndExclam(x, y) 
})(1)
add1AndExclamV2(2)

"3!"`

imz -- Ivan Zakharyaschev
  • 4,921
  • 6
  • 53
  • 104
Wumbo
  • 83
  • 10
  • 1
    Curry can't tell the number of parameters because there is no static type system for that in javascript, and every function is variadic. The `.length` property of functions is only a hint, and it's a hazzle to set it up as expected in a `compose` function for variadic, uncurried functions. Ramda goes through this (but fails rarely), while Underscore/Lodash don't. `compose` will work as expected if you `curry` your functions beforehand. – Bergi Aug 19 '16 at 21:24

2 Answers2

0

var myAdd = function(x, y) { return x + y; }; var exclam = function(x) { return x.toString() + '!'; }; var addSclam = pipe(myAdd, exclam); addSclam(1,2); // "3!" var add1Sclam = curry( addSclam )(1); add1Sclam(500); // "501!"

It looks like ramda does what would have been intuitive to me. It adds another 1.2mb to my project, but I guess it could be used to more or less replace lodash.

Wumbo
  • 83
  • 10
  • This question is wrought with a lot of confusion. You're trying to compose a binary function `myAdd` with a unary function `exclam` and therefore you're having trouble. Rambda is not a *solution* for you in this case. It has only allowed your misunderstanding of functional programming to persist. – Mulan Aug 19 '16 at 19:09
  • If you think Rambda's `pipe` is giving you a "real compose function" or that it's doing anything more "intuitive", you'll have deceived yourself again. – Mulan Aug 19 '16 at 19:19
  • add and pow are used in the examples of flow and pipe and compose, I think it's a standard idea. The only thing that matters is the outer function called returns a unary to feed down the pipe, not that the outer function can't have more than one argument. The examples that show you how to write the compose function yourself seem to make that clear. So my question to you is what situation makes things hard when the outer function takes two args? What am I not getting? Maybe you are confused by the arg order of pipe and flow; They're the reverse of the canonical compose. – Wumbo Aug 19 '16 at 20:30
  • Using a binary function in a composition like you've done doesn't highlight the problem. `h(g(f(x,y)))` looks like it works out because binary function `f` still has one return value that threads through the unary `g` and unary `h`. What happens when you try to compose several binary functions? Using `_` as a visual placeholder for any argument, is this correct `m(l(k(_,_),_),_)`? or is this m(_,l(_,k(_,_)))`? How about ternary functions? Which *position* should the composed function take in the outer function's params? ... – Mulan Aug 19 '16 at 20:39
  • ... And what if the position isn't uniform for each function? There isn't a correct answer, and any generic utility that supports this kind of "composition" is just reinforcing bad design. – Mulan Aug 19 '16 at 20:40
  • Thanks for the explanation. I think if I wanted to compose a special function where one of the nested functions took two args, I would write it out. If I wanted to compose with the outer function, I would curry it. The point is just making higher order functions, I thought, not that the higher order function needs to be immediately suitable for the compose function. I having been thinking of compose as a shortcut to clean up what's written, so of course it can't handle a complex case like nested funcs with multiple args. I guess maybe thinking of it like that is not functional, but unixy. – Wumbo Aug 19 '16 at 20:45
  • I added an answer to the question which might help – Mulan Aug 19 '16 at 20:50
0

This is just basic function composition. Lodash "flow" and Rambda "pipe" probably found it difficult to name these functions because they're not suitably generic. They're not "real" the same way you use the word real in the phrase "real compose function".

You can compose a binary function with a unary function using comp2 – the catch is, the "binary" function must be in curried form instead of taking a tuple

let f = x => y => ... 

instead of

let f = (x,y) => ...

Recall that functional programming has its roots in the lambda calculus where there is no such thing as a function that takes anything other than 1 argument.

const curry = f => x => y => f (x,y)
const comp = f => g => x => f (g (x))
const comp2 = comp (comp) (comp)

var add = function(x, y) { return x + y };
var exclam = function(x) { return x.toString() + "!"; }

console.log(exclam (1)) // "1!"
console.log(add (1,2)) // 3

var add1 = curry (add) (1)
console.log(add1 (4)) // 5

var addAndExclam = comp2 (exclam) (curry (add))
console.log(addAndExclam (1) (2)) // "3!"

I encourage you to use the substitution model to see how the expression evaluates


Putting types on everything helps you reason about the program more effecitvely

// curry :: ((a,b) -> c) -> a -> b -> c
const curry = f => x => y => f (x,y)

// comp :: (b -> c) -> (a -> b) -> (a -> c)
const comp = f => g => x => f (g (x))

// comp2 :: (c -> d) -> (a -> b -> c) -> (a -> b -> d)
const comp2 = comp (comp) (comp)

// add :: (Number,Number) -> Number
var add = function(x, y) { return x + y };

// exclam :: Number -> String
var exclam = function(x) { return x.toString() + "!"; }

console.log(exclam (1)) // "1!"
console.log(add (1,2)) // 3

// add1 :: Number -> Number
var add1 = curry (add) (1)
console.log(add1 (4)) // 5

// addAndExlam :: Number -> Number -> String
var addAndExclam = comp2 (exclam) (curry (add))
console.log(addAndExclam (1) (2)) // "3!"

Regarding your comment:

I think if I wanted to compose a special function where one of the nested functions took two args, I would write it out

Good idea. If you find yourself looking around for a built-in procedure (provided by your language or some library), that should already be an indicator to you that you should try to write it out first. At least confirm with yourself that you're understanding your needs correctly.

This is perfectly acceptable

const addAndExclam = (x,y) => exclam (add (x,y))

And so is this

const addAndExclam = x => y => exclam (add (x,y))

If you later learn about comp2 and see that it could describe the code a little better, then you can implement it at that time

const addAndExclam = comp2 (exclam) (curry (add))
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • Ramda's `pipe` is a *real* composition function, it's named that way because `compose` with flipped parameters is known as "pipe". – Bergi Aug 19 '16 at 21:17
  • I had to play with it in the console for a bit, but I think I've finally got my head wrapped around it. So ramda is doing a bit of extra behind the scenes to make it more flexible. But with your definition of comp (which is the one I've seen in videos) there's no way to tell that the final function returned should take more than one arg. I would have to explicitly make a compose which expected the function to be called with 2. comp = f => g => (x, y) => f (g (x, y)). That's interesting. – Wumbo Aug 19 '16 at 21:51
  • On the other hand, I can just splat args and there's no problem at all. comp = f => g => function() { return f (g.apply(undefined, [].concat(Array.prototype.slice.apply(arguments || [])))) } ---- (splat stolen from crockford. I guess it's ...arguments in ES6) – Wumbo Aug 19 '16 at 22:05
  • The main disadvantage of splat being the same disadvantage of the flow, it returns a function that doesn't know how many args it should have and so can't be curried. This has definitely given me more insight on why this happens. I'm not sure I'm convinced that pipe's flexibility for the edge case is a bad thing though. – Wumbo Aug 19 '16 at 22:13