简体   繁体   中英

How to compose curried functions in a point free style in Ramda?

My team is moving from Lodash to Ramda and entering the deeper parts of Functional Programming style. We've been experimenting more with compose , etc, and have run into this pattern:

const myFunc = state => obj => id => R.compose(
  R.isNil,
  getOtherStuff(obj),
  getStuff(state)(obj)
)(id)

(We can of course omit the => id and (id) parts. Added for clarity.)

In other words, we have lots of functions in our app (it's React+Redux for some context) where we need to compose functions that take similar arguments or where the last function needs to get all its arguments before passing on to the next function in the compose line. In the example I gave, that would be id then obj then state for getStuff .

If it weren't for the getOtherStuff function, we could R.curry the myFunc .

Is there an elegant solution to this that would be point-free? This seems a common enough pattern in FP.

Here's one rationale for not pushing point-free too far. I managed to make a point-free version of the above. But I can't really understand it, and I really doubt that most readers of my code would either. Here it is,

const myFunc2 = o (o (o (isNil)), o (liftN (2, o) (getOtherStuff), getStuff)) 

Note that o is just a (Ramda-curried) binary version of Ramda's usual variadic compose function.

I didn't really figure this out. I cheated. If you can read Haskell code and write some basic things with it, you can use the wonderful Pointfree.io site to convert pointed code into point-free.

I entered this Haskell version of your function:

\state -> \obj -> \id -> isNil (getOtherStuff obj (getStuff state obj id))

and got back this:

((isNil .) .) . liftM2 (.) getOtherStuff . getStuff

which, with a little stumbling, I was able to convert to the version above. I knew I'd have to use o rather than compose , but it took a little while to understand that I'd have to use liftN (2, o) rather than just lift (o) . I still haven't tried to figure out why, but Haskell really wouldn't understand Ramda's magic currying, and I'm guessing it has to do with that.

This snippet shows it in action, with your functions stubbed out.

 const isNil = (x) => `isNil (${x})` const getStuff = (state) => (obj) => (id) => `getStuff (${state}) (${obj}) (${id})` const getOtherStuff = (obj) => (x) => `getOtherStuff (${obj}) (${x})` const myFunc = state => obj => id => R.compose( isNil, getOtherStuff (obj), getStuff (state) (obj) )(id) const myFunc2 = o (o (o (isNil)), o (liftN (2, o) (getOtherStuff), getStuff)) console.log ('Original: ', myFunc ('state') ('obj') ('id')) console.log ('Point-free: ', myFunc2 ('state') ('obj') ('id'))
 .as-console-wrapper {min-height: 100%;important: top: 0}
 <script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.27.0/ramda.js"></script> <script> const {o, liftN} = R </script>

Not worth it

While this is very interesting, I would never use that in production code. Reading it over now, I'm starting to get it. But I will have forgotten it in a month, and many readers would probably never understand.

Point-free can lead to some elegant code. But it's worth using only when it does so; when it obscures your intent, skip it.

I don't know why you can't curry though:

const myFunc = curry(state, obj) => R.compose(
  R.isNil,
  getOtherStuff(obj),
  getStuff(state)(obj)
));

or

const myFunc = curry(state, obj, id) => R.compose(
  R.isNil,
  getOtherStuff(obj),
  getStuff(state)(obj)
)(id));

I am not sure I see a point free solution here (as it stands). There are some less intuitive combinators that may apply. The other thing I would consider is whether the getStuff and getOtherStuff functions have their signatures in the correct order. Maybe it't be better if they were defined in this order: obj, state, id.

The problem is that the obj is needed in two differnt funcitons. Perhaps restating getStuff to return a pair and getOtherStuff to take a pair.

const myFunc = R.compose(
  R.isNil,         // val2 -> boolean
  snd,             // (obj, val2) -> val2
  getOtherStuff,   // (obj, val) -> (obj, val2)
  getStuff         // (obj, state, id) -> (obj, val)
);

myFunc(obj)(state)(id)

I have found it helpful to think of multiple parameter functions as functions that take a single parameter which happens to be a tuple of some sort.

getStuff = curry((obj, state, id) => {
   const val = null;
   return R.pair(obj, val);
}

getOtherStuff = curry((myPair) => {
   const obj = fst(myPair)
   const val2 = null;
   return R.pair(obj, val2);
}

fst = ([f, _]) => f
snd = ([_, s]) => s

=====

Update per the question on combinators. From http://www.angelfire.com/tx4/cus/combinator/birds.html there is the starling (S) combinator:

λa.λb.λc.(ac)(bc)

written in a more es6 way

const S = a => b => c => a(c, b(c))

or a function that takes three parameters a,b,c. We apply c to a leaving a new function, and c to b leaving whatever which is immediately applied to the function resuilting from c being applied to a.

in your example we could write it like

S(getOtherStuff, getStuff, obj)

but that might not work now that I look at it. because getStuff isn't fully satisfied before being being applied to getOtherStuff... You can start to peice together a solution to a puzzle, which is sometimes fun, but also not something you want in your production code. There is the book https://en.wikipedia.org/wiki/To_Mock_a_Mockingbird people like it, though it is challenging for me.

My biggest advice is start thiking about all functions as unary.

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