Reputation: 1280
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.
Upvotes: 0
Views: 549
Reputation: 6886
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.
Upvotes: 1
Reputation: 50807
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>
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.
Upvotes: 2