问题
In case it matters this is about functional programming in JavaScript and in my examples I’ll be using Ramda.
While everybody at work has fully embraced functional programming, there’s also a lot of discussions around how to do it “right”.
These two functions will do exactly the same thing: take a list and return a new list in which all strings have been trimmed.
// data-centric style
const trimList = list => R.map(R.trim, list);
// point-free style
const trimList = R.map(R.trim);
So far so good. However with a more complex example, the difference between the two styles is striking: take a list and return a new list in which all strings are equal to a property found in an object.
var opts = {a: 'foo', b: 'bar', c: 'baz'};
var list = ['foo', 'foo', 'bar', 'foo', 'baz', 'bar'];
myFilter(opts, 'a', list); //=> ["foo", "foo", "foo"]
myFilter(opts, 'b', list); //=> ["bar", "bar"]
// data-centric style
const myFilter = (opts, key, list) => {
var predicate = R.equals(opts[key]);
return R.filter(predicate, list);
};
// point-free style
const myFilter = R.converge(
R.filter, [
R.converge(
R.compose(R.equals, R.prop), [
R.nthArg(1),
R.nthArg(0)]),
R.nthArg(2)]);
Besides readability and personal taste, are there any reliable evidences to suggest that one style is better suited than the other in some circumstances?
回答1:
I know of no evidence that demonstrates advantages of one style over the other. But there is a clear trend in the history of programming towards higher abstractions... and an equally clear history of resistance to this trend. A move from Assembly to Fortran or LISP was a move up the abstraction stack. The use of SQL rather than bespoke B-tree retrievals was a another. The move to FP, both within a language like Javascript and in the changing landscape of programming languages, is to my mind a similar move.
But much of that has to do with elements more fundamental than this syntactic decision: equational reasoning means that we can build our own abstractions on top of more solid footing. So purity and immutability are essential; point-free is merely nice to have.
That said, it is often simpler. And that is important. Simpler code is easier to read, easier to modify. Note that I distinguish between simple and easy -- the distinction articulated in the classic talk by Rich Hickey. Those new to the style will often find it more confusing; so too the assembly programmers who abhorred the next generation of language and all their ilk.
By not defining intermediate variables, by not even specifying arguments that can be inferred, we can significantly improve simplicity.
It's hard argue that this:
const foo = (arg) => {
const qux = baz(arg)
return bar(qux)
}
or even this:
const foo = (arg) => bar(baz(arg))
is simpler than this:
const foo = compose(bar, baz)
And that's because while all three involve these notions:
- function declaration
- function reference
the second one adds also:
- argument definition
- function body
- function application
- nesting of function calls
and the first version has:
- argument definition
- function body
- function application
- local variable definition
- local variable assignment
- the
return
statement
while the third one adds only
- function composition
If simpler means having fewer notions entwined, the point-free version is simpler, even if it's less familiar to some people.
In the end, much of this comes down to readability. You spend more time reading your own code more than you do writing it. Anyone else spends a lot more time reading it. If you write code that it is simple and readable, you've made the experience much better for everyone. So where point-free code is more readable, use it.
But don't find it necessary to remove every point in order to, ahem, prove a point. It's easy to fall into the trap of trying to make everything point-free just because you can. We already know that it's possible; we don't need to see the gory details.
回答2:
The academic term is eta conversion. When you have a function with redundant lambda abstraction like
const trim = s => s.trim();
const map = f => xs => xs.map(x => f(x));
const trimList = xs => map(trim) (xs); // lambda redundancy
you can simply strip the last lamdba abstraction by eta reduction:
const trimList = map(trim);
When you use eta reduction extensively, you end up with point free style. However, both versions are perfectly fine in the functional paradigm. It's just a matter of style.
Actually, there are at least two reasons to use eta abstraction (the opposite of eta reduction) in Javascript:
- to fix Javascript's multi argument functions like I did with
map = f => xs => xs.map(x => f(x))
- to prevent immediate evaluation of expressions/statements (lazy evaluation effect) as in
recur = f => x => f(recur(f)) (x)
回答3:
there are some good answers and in my opinion, mixing the two styles is the way to go.
the last point-free style example is a little bit confusing, you can make it less confusing :
const myFilter = converge(
filter,
[compose(equals , flip(prop)) , nthArg(2)]
)
来源:https://stackoverflow.com/questions/53670665/when-is-it-appropriate-to-choose-point-free-style-vs-a-data-centric-style-in-fun