And while many devs are resistant to try functional ways, this first example reads so much better than the original code that I find it impossible to believe that some prefer the imperative loop/conditional nesting approach.
And while many devs are resistant to try functional ways, this first example reads so much better than the original code that I find it impossible to believe that some prefer the imperative loop/conditional nesting approach.
The map and filter methods are nice too, but they're for one-liners.
Writing with goto was the idiomatic way before Algol and structural programming came.
Having only a handful of scalar types was the idiomatic way until structural data types came (and later objects).
Writing programs as fragments of text that get glued together somehow at build time was the idiomatic way until module systems came. (C and partly C++ continue to live in 1970s though.)
Callback hell was the idiomatic way to do async until Futures / Promises and appropriate language support came.
Sometimes it's time to move on. Writing idiomatic ES5 may feel fun for some, but it may not be the best way to reach high productivity and correctness of the result.
Is this just fashion? Is there a way to settle it other than “I like it better?”
Yes.
Readability is a characteristic of the reader, not what is being read.
This simple truth seems to be so hard for many people to internalize. My theory as to why is that most programmers never get exposed to drastically different and unfamiliar languages and styles of programming. If they were forced to confront and internalize 2-3 different ways of writing code, they would realize this truth.
Personally, I once thought Lisp was unreadable... until I learned it. I once thought BASH was unreadable... until I learned it. Same with half a dozen other languages. Same for styles. "Readability" is just a familiarity and proficiency of the reader.
There is no way that
name: R.pipe(R.prop('name'), R.toUpper),
age: R.prop('age'),
isAdult: R.always(true)
is more readable than name: user.name.toUpperCase(),
age: user.age,
isAdult: trueBut instead of
for (const i=0; i < data.length; i++) {
new_data[i] = old_data[i].toUpperCase();
}
you can write const new_data = old_data.map((x) => x.toUpperCase());
I think it's both more clear and less error-prone.https://www.typescriptlang.org/play/?#code/MYewdgzgLgBCA2ATA...
All syntactically valid Javascript is also syntactically valid Typescript, it just adds stuff, though you can get runtime errors for things like reassigning variables in a way Javascript is fine with that Typescript disallows.
All I get out of that is that you like functional code.
Both sets of code were fine, but I understood the loop variant instantly, while it took me a bit longer with the FP code.
As a side note: The only real JS I did was optimising some performance critical code, and I did have to refactor a number of FP chains back to loops. This was because the FP way keeps constructing a new list for each step which was slow.
Futhermore, in JS, the functionnal style is less performant (nearly twice on my machine, i assume because it do less useless memory allocations)
So, same functionnality, readable by more people, more performant? The imperative example seems like the better code.
function processUsers(users: User[]): FormattedUser[] {
let adults = users.filter(user => user.age >= 18);
return adults.map(user => FormattedUser.new_adult(user));
}
On the performance tip, what scale are we talking? Is it relevant to the target system? Obviously the example is synthetic, so we can't know that, but does it seem like this would have a runtime performance that is meaningful in some sort of reasonable use case?IMO, this is a simple consequence of technology moving faster than society. There are still instructors out there who learned to program in an environment where the go-to options for imperative programming were C and FORTRAN; the go-to options for other paradigms (if you'd even heard of other paradigms) were things like Lisp, Haskell and Smalltalk; and CPU speeds were measured in MHz on machines that you had to share with other people. Of course you're going to get more experience with imperative programming; and familiarity breeds comprehension.
But really, I believe strongly that the functional style - properly factored - is far more intuitive. The mechanics of initializing some output collection to a default state (and, perhaps, the realization that zero isn't a special case), keeping track of a position in an input collection, and repeatedly appending to an output, are just not that interesting. Sure, coming up with those steps could be a useful problem-solving exercise for brand-new programmers. But there are countless other options - and IMX, problem-solving is fiendishly hard to teach anyway. What ends up happening all the time is that you think you've taught a skill, but really the student has memorized a pattern and will slavishly attempt to apply it as much as possible going forward.
> Futhermore, in JS, the functionnal style is less performant (nearly twice on my machine, i assume because it do less useless memory allocations)
Sure. Meanwhile in Python:
$ python -m timeit "x = []" "for i in 'example sequence':" " x.append(i)"
500000 loops, best of 5: 796 nsec per loop
$ python -m timeit "x = [i for i in 'example sequence']"
500000 loops, best of 5: 529 nsec per loop
... But, of course: $ python -m timeit "x = list('example sequence')"
2000000 loops, best of 5: 198 nsec per loop
Horses for courses.In this case, you first declare the end-goal - visit a friend and have full gas tank, with the actual steps to achieve them being much less important and often left to be defined at a later point (e.g. which particular gas station, which particular pump etc.). This corresponds more to functional thinking.
An imperative thinking would correspond more to "I will sit in the car, start the engine, ride on highway, stop at address X, converse with Y, leave 2 hours later, stop at gas station X" - in this case the imperative steps are the dominant pattern while the actual intent (visit a friend) is only implicit.
I certainly don't think like that. My main goal is to visit a friend. The transportation is subordinate, it's only a mean to the goal, an implementation detail which I don't care about much. I might even take a train instead of driving the car, or even ride a bike, if I feel like it and the weather is nice on the day of the visit.
Now reflecting on this, I think such focus on the process (as opposed to focus on the goal), exact imperative order, not being able to alter the plan even if the change is meaningless in relation to the goal, is a sign of autism. But I don't believe most people think like that.
I think most people think more or less like that, and that it is not "a sign of autism".
Say if I want to filter a sequence of users and omit users below the age of 18, I'll construct my predicate (a "what"), and want to apply that predicate to create a new sequence of user (another "what").
I really don't want to tell a computer how to process a list every single time. I don't care about creating an index first and checking the length of my list in order to keep track that I process each user in my list sequentially, and don't forget that important "i++". All I want at that moment is to think in streams, and this stream processing can happen in parallel just as well for all I care.
But I also do think Python, Haskell etc. are the most expressive here with list comprehensions. It can't get more concise than this IMHO:
users_adult = [
user
for user in users
if user.age >= 18
]The caveat here is to which extent computers have tinted their models of a brain, but they are professional cognitive researchers so I’d give them the benefit of the doubt :)
It's not more intuitive for the entire system.
When you make a new file in a file system, you're already violating functional programming, even if you atomically create that file with all the content specified, and make it immutable.
You must construct a new file system which is like the old one, but with that file, and then have the entire system tail call into a world where you pass that new file system as a parameter, so that the old one is not known (garbage).
Unix has been the most successful system in getting partial functional programming into ordinary people's hands.
A Unix pipeline like < source-file | command | command | ... | command > dest-file is functional except for the part where dest-file is clobbered. Or at least can be functional. The commands can have arguments that are imperative programs (e.g. awk) but the effects are contained.
In the famous duel between Doug McIlroy and Knuth in solving a problem, in which McIlroy wrote a concise Unix script combining a few tools, McIlroy's solution can be identified as functional:
tr -cs A-Za-z '\n' |
tr A-Z a-z |
sort |
uniq -c |
sort -rn |
sed ${1}q
Nowhere is there any goto statement or assignment.With imperative programming you can follow a program step by step, line by line, it can be done with a debugger, pen and paper, or in your head.
With functional programming, not so much. It runs functions. What do these functions do? Don't know, they are pieced up from someplace else in the code. And thanks to lazy evaluation, they may not even exist. In the design phase, it is mostly a good thing, as it is flexible, and pure functions are less likely to make a mess than functions with side effects, but there will be a point where the program will not behave as it should, no matter your paradigm. And that's when it becomes a problem.
It is also a problem with object programming if you abuse abstraction, in fact, it is a general problem with abstraction, but functional programming makes it the default, whereas imperative programming is concrete by default.
As for the Python example, I am a bit surprised that the optimizer didn't catch it, all three are common and equivalent constructs that could have been replaced by the most performant implementation, presumably the third one. But well, optimizers are complicated.
I disagree. For-cycles are usually more difficult to reason about, because they're more general and powerful. If I see "for (...", I only know that the subsequent code will iterate, but the actual meaning has to be inferred from the content.
Meanwhile, a .map() or .filter() already give me hints - the lambda will transform the values (map), will filter values (filter), these hints make it easier to understand the logic because you already understand what the lambda is meant to do.
Other benefits stem from idiomatic usage of these constructs. It's normal to mix different things into one for-cycle - e.g. filtering, transformation, adding to the resulting collection are all in the same block of code. In the functional approach, different "stages" of the processing are isolated into smaller chunks which are easier to reason about.
Another thing is that immutable data structures are quite natural with functional programming and they are a major simplification when thinking about the program state. A given variable has only one immutable state (in the current execution) as opposed to being changed 1000 times over the course of the for-loop.
And then someone slaps do {} while(0) in a macro.
Yeah, there’s your problem. This is in fact possible!
As for whether or not it's possible at all to combine a map and filter into a single loop I guess depends on whether the first operation can have side effects that affect the second operation or the collection that is being iterated over. I don't know the answer, but I would be surprised if there wasn't some hard to detect corner case that prohibits this kind of optimization.