Reputation: 39
I'm learning about the JS Library Ramda rightnow. And there seems to be a pattern i am unable to resolve properly. For example i have this code.
const filterPayments = filter => payments =>
r.filter(
r.allPass([
typePred(filter),
amountPred(filter),
accountPred(filter),
currencyPred(filter)
]),
payments
);
Now this seems to be a case where i'm not to far off beeing able to make it point free. I just can't seem to find the right way.
In addition i have troubles making logical functions like this completely functional and Point-free:
const amountPred = filter => p =>
r.and(p.betrag >= filter.amount.min, p.betrag <= filter.amount.max);
Maybe somebody can point me into the right direction or offer some guidance?
Upvotes: 0
Views: 246
Reputation: 50787
I had started to do the same sort of work that Scott Christopher did, and refactored the main function to point-free, knowing from the start that my advice would almost certain to be to not bother with point-free for this. I found my point-free version, which was simpler than I expected. Before I started on the predicates, I looked to see Scott's solution.
He did much what I would have done, and probably better than I would have. And of course he ends with similar advice. I always try to suggest that people don't make a fetish out of point-free. Use it when it makes your code cleaner and more understandable. Don't use it when it doesn't.
I wouldn't answer at all, except that I ended up with a slightly nicer point-free version of the main function, and would like to offer that up for consideration:
const filterPayments = pipe(
juxt([typePred, amountPred, accountPred, currencyPred]),
allPass,
filter
)
Although this is fairly clean, I don't think it's as readable as your original, and so I wouldn't recommend it. But if you really want point-free, this is a possibility.
Upvotes: 2
Reputation: 6516
The guidance I'd offer is to stick with what you currently have :)
I can show you an example of what point-free translations of what you have there could look like, though the resulting code is quite horrible to read and is really only good for a session of mental gymnastics.
First up, payments
appears in the final position of both sides of the function expression so we can simply drop that.
const filterPayments = filter =>
R.filter(
R.allPass([
typePred(filter),
amountPred(filter),
accountPred(filter),
currencyPred(filter)
]));
Next we can look to deal with the list of functions passed to R.allPass
R.allPass(R.map(p => p(filter), [typePred, amountPred, accountPred, currencyPred]))
Now we can begin to remove the filter
argument by flipping R.map
to push it towards the end.
R.allPass(R.flip(R.map)([typePred, amountPred, accountPred, currencyPred])(p => p(filter))
Now lets make filter
and argument to p => p(filter)
instead of closing over it, making it:
R.allPass(R.flip(R.map)
([typePred, amountPred, accountPred, currencyPred])
(R.applyTo(filter)))
This is now starting to take shape like h(g(f(a)))
which we can transform to point-free using R.pipe
, R.compose
, R.o
, etc.
const filterPayments = R.compose(
R.filter,
R.allPass,
R.flip(R.map)([typePred, amountPred__, accountPred, currencyPred]),
R.unary(R.applyTo) // R.unary is needed here to tell Ramda only to expect the first argument in this composition
)
This gives us a function that should now be equivalent to your filterPayments
in point-free form.
Your amountPred
example gets even more convoluted and I think you'll agree afterwards that what you already have is a much better choice between the two.
Again, we start by trying to push arguments to the end of each side of the expression:
filter => p =>
both(
x => x >= filter.amount.min,
x => x <= filter.amount.max
)(p.betrag)
And then we can drop the p
:
filter => R.o(
both(
x => x >= filter.amount.min,
x => x <= filter.amount.max
),
prop('betrag'))
The functions passed to both
can also be swapped out:
filter => R.o(
both(
R.flip(R.gte)(filter.amount.min),
R.flip(R.lte)(filter.amount.max)
),
prop('betrag'))
Now to tackle filter
, we can use R.converge
to pass the same argument to multiple functions and then combine the results of each together.
filter => R.o(
R.converge(both, [
f => R.flip(R.gte)(R.path(['amount', 'min'], f),
f => R.flip(R.lte)(R.path(['amount', 'max'], f)
])(filter),
prop('betrag'))
And now the f
is in the end position, so it can be dropped by composition:
filter => R.o(
R.converge(both, [
R.o(R.flip(R.gte), R.path(['amount', 'min'])),
R.o(R.flip(R.lte), R.path(['amount', 'max']))
])(filter),
prop('betrag'))
Getting filter
to the end is where it gets quite confusing, involving flipping composition functions.
filter => R.o(
R.flip(R.o)(R.prop('betrag')),
R.converge(both, [
R.o(R.flip(R.gte), R.path(['amount', 'min'])),
R.o(R.flip(R.lte), R.path(['amount', 'max']))
])
)(filter)
And finally...
const amountPred = R.o(
R.flip(R.o)(R.prop('betrag')),
R.converge(both, [
R.o(R.flip(R.gte), R.path(['amount', 'min'])),
R.o(R.flip(R.lte), R.path(['amount', 'max']))
])
)
... comparing that to your original, I know which I prefer to read:
const amountPred = filter => p =>
p.betrag >= filter.amount.min && p.betrag <= filter.amount.max
Upvotes: 3