3/11/2025 at 4:27:50 PM
> Chaining together map/reduce/filter and other functional programming constructs (lambdas, iterators, comprehensions) may be concise, but long/multiple chains hurt readabilityThis is not at all implied by anything else in the article. This feels like a common "I'm unfamiliar with it so it's bad" gripe that the author just sneaked in. Once you become a little familiar with it, it's usually far easier to both read and write than any of the alternatives. I challenge anyone to come up with a more readable example of this:
var authorsOfLongBooks = books
.filter(book => book.pageCount > 1000)
.map(book => book.author)
.distinct()
By almost any complexity metric, including his, this code is going to beat the snot out of any other way of doing this. Please, learn just the basics of functional programming. You don't need to be able to explain what a Monad is (I barely can). But you should be familiar enough that you stop randomly badmouthing map and filter like you have some sort of anti-functional-programming Tourette's syndrome.
by feoren
3/11/2025 at 7:23:21 PM
This comment seems unnecessarily mean-spirited... perhaps I just feel that way because I'm the person on the other end of it!I agree the code you have there is very readable, but it's not really an example of what that sentence you quoted is referencing... However I didn't spell out exactly what I meant, so please allow me to clarify.
For me, roughly 5 calls in a chain is where things begin to become harder to read, which is the length of the example I used.
For the meaning of "multiple", I intended that to mean if there are nested chains or if the type being operated on changes, that can slow down the rate of reading for me.
Functional programming constructs can be very elegant, but it's possible to go overboard :)
by seeinglogic
3/12/2025 at 9:07:29 AM
To me the functional style is much more easy to parse as well. Maybe the lesson is that familiarity can be highly subjective.I for example prefer a well chosen one-liner list comprehension in python over a loop with temporary variables and nested if statements most of the time. That is because usually people who use the list comprehension do not program it with side effects, so I know this block of code, once understood stands for itself.
The same is true for the builder style code. I just need to know what each step does and I know what comes out in the end. I even know that the object that was set up might become relevant later.
With the traditional imperative style that introduces intermediate variables I might infer that those are just temporary, but I can't be sure until I read on, keeping those variables in my head. Leaving me in the end with many more possible ways future code could play out. The intermediate variables have the benefit of clarifying steps, but you can have that with a builder pattern too if the interface is well-chosen (or if you add comments).
This is why in an imperative style variables that are never used should be marked (e.g. one convention is a underscore-prefix like _temporaryvalue — a language like Rust would even enforce this via compiler). But guess what: to a person unfamilar with that convention this increases mental complexity ("What is that weird name?"), while it factually should reduce it ("I don't have to keep that variable in the brain head as it won't matter in the future").
In the end many things boil down to familiarity. For example in electronics many people prefer to write a 4.7 kΩ as 4k7 instead, as it prevents you from accidentally overlooking the decimal point and making an accidental off-by-a-magnitude-error. This was particularly important in the golden age of the photocopier as you can imagine. However show that to a beginner and they will wonder what that is supposed to mean. Familiarity is subjective and every expert was once a beginner coming from a different world, where different things are familiar.
Something being familiar to a beginner (or someone who learned a particular way of doing X) is valuable, but it is not necessarily an objective measure of how well suited that representation is for a particular task.
by atoav
3/12/2025 at 2:48:05 PM
> Maybe the lesson is that familiarity can be highly subjective.Over the years I've come to firmly believe that readability is highly subjective. And familiarity is a key contributor to that, but not the only one. There are other factors that I've found highly correlate with various personality traits and other preferences. In other words, people shouldn't make claims that one pattern is objectively more readable than another. Ever.
I've reached the point where anyone who claims some style is "more readable" without adding "to me" I just start to tune out. There's very little objective truth to be had here.
What should one do? If on a team and you're the outlier, suck it up and conform. If you're on a team and someone else is the outlier? Try to convince them to suck it up and conform. If you're on a new team? Work empathetically with your teammates to understand what the happy medium style should be.
by jghn
3/13/2025 at 8:30:15 AM
Pragmatic, but good, advice that I would recommend anybody to follow in their daily pracrise.However I'd defend the notion that on the bad end of things you can have such a thing as (objectively?) hard to read code. With "hard to read" I do not mean "nobody can figure it out with time", what I mean is, that figuring it out takes 99% of programmers longer than the equivalent in another language or style. As you rightly point out, it is important to realize that this is a statistical realization and not an universal law of nature, so it really matters on which cohort of people you look at and recommendations stemming from such observations should be taken with a grain of salt.
Yet, underneath all of that isn't there such a thing as truly objectively hard to read style? Brainfuck for example is an objectively hard to read language – they put that even into the name of the language. Does that mean there is not a single person who can read it fluently? Probably not, but that doesn't invalidate the point. A double black diamond ski track is objectively harder to ski, exactly because there are less people that are able to ski it.
If you see programming as working with language and symbols to achieve some behavior, it is clear that there are patterns who match more with the known (familiar) of most people. If you ask non-programmers or non-mathematicians how to describe some action in any way they like they will probably use their daily language. That means code that looks somewhat similar to how a regular person would write it down is surely on the "very familiar"-end of things. Now I did argue that maximum familiarity to regular people is not in itself a desireable goal, we need to make a tradeoff between familiarity and suitability to express program structures and operations. The latter is not how regular people think at all, so using them as an absolute guide isn't a good idea. However thinking about how to write things that they are both expressive in terms of programming behavior and easy to reason about is a desireable goal. There just isn't a single right way of doing that and sometimes good enough is just that.
by atoav
3/13/2025 at 2:35:09 PM
For sure. I think we're channeling similar sentiments. Obviously there is *some* amount of objectivity here. No one is going to look at a million lines of Brainfuck code and say "this is easy to grok". it's just that the problem is these objective truths make up a tiny fraction of the crap people debate when these topics come up.And yes, the maximal familiarity is a huge aspect. Many years ago I got into an argument with a colleague regarding FP patterns. He contended that they were objectively harder to reason about and cited the difficulty of new hires in ramping up. I was contending that this is untrue, but rather those new hires had existed in a world where they didn't often encounter FP patterns. For practical purposes the end result is the same, I'll admit. But to your point: if we accept that the missing ingredient is familiarity vs it being objectively bad it changes how one might approach the problem.
by jghn
3/16/2025 at 7:23:05 PM
Funny, as years pass by I slide more and more towards the opposite. For most aspects of readability people discuss, there are objectively correct choices, with some leeway for the specific circumstances.Code that interleaves high and low level concerns, that has a lot of variables used all over the place, is deeply nested, etc. vs code that is modular, keeps the nesting shallow, splits up the functionality so that concerns are clearly separated and variables have the smallest scopes possible.
One of these styles is better than the other and it's not subjective in the least, so long as you're a human. We all have roughly the same limitations in terms of e.g. memory capacity so ways of programming that reduce the need to keep a lot of stuff in memory at a time will be objectively better.
> I've reached the point where anyone who claims some style is "more readable" without adding "to me" I just start to tune out. There's very little objective truth to be had here.
Adding qualifiers all over the place is just a defensive writing style, best fitting online forums like this one where people will nitpick everything apart. However, it's not reasonable to pay so much attention to someone's particular word choices.
Objective truth is very easy to find actually. The problem you're solving is objective and the desired outcome is too. It's just a matter of analysing the problem space to find the correct design. The feeling of subjectivity largely just comes from the high complexity of the problem space.
> If on a team and you're the outlier, suck it up and conform.
It's way more complicated than you make it sound. It's entirely possible the entire team is wrong.
A general strategy when joining a new team is to follow what the team does exactly and not attempt to make changes until you learn why the team does things the way they do (Chesterton's Fence). Once you understand, you can start suggesting improvements.
by Mawr
3/14/2025 at 7:48:44 PM
> In other words, people shouldn't make claims that one pattern is objectively more readable than another. Ever.Maybe you haven't seen the sort of code I've seen.
Consider this specimen, for example:
// Call a function.
// Function call syntax isn't very visible (only a couple parens tacked onto an identifier);
// this is a major win for readability.
function callFunction(func, ...args) {
return func(...args);
}
// Perform math.
// Math symbols are arcane and confusing, so this makes it much more clear.
// I'm trying to program here, not transmute lead into gold!
function doMath(left, op, right) {
const OPS = {
"less than or equal to": (l, r) => l <= r,
"minus": (l, r) => l - r,
};
// TODO: add support for other operators.
return callFunction(OPS[op], left, right));
}
const MEMO_TABLE = {}
function fibonacci(n) {
if (callFunction(doMath, n, "less than or equal to", 1)) {
const result = n;
// Memoization makes this fibonacci implementation go brrrrrrrrr!
MEMO_TABLE[n] = result;
return result;
} else {
const result = callFunction(fibonacci, callFunction(doMath, n, "minus", 1)) + callFunction(fibonacci, callFunction(doMath, n, "minus", 2));
// Memoization makes this fibonacci implementation go brrrrrrrrr!
MEMO_TABLE[n] = result;
return result;
}
}
Yes, it was an intentional choice to never actually make use of MEMO_TABLE -- this (reproduced from memory) is a submission from an applicant many, many years ago (the actual code was much worse, it was somehow 3 to 4 times as many lines of code).Compared to:
function fibonacci(n) {
if (n <= 1) {
return n;
} else {
return fibonacci(n - 1) + fibonacci(n - 2);
}
}
> In other words, people shouldn't make claims that one pattern is objectively more readable than another. Ever.Taken literally, I can't argue with this. But if you amend that to "people shouldn't make claims that one pattern is objectively more readable than another to competent individuals" (which I suspect is intended to be implied by the original quote), then I'd have to disagree. The choice between functional vs procedural may mostly be subjective, but a convoluted pattern is objectively worse than a non-convoluted one (when the reader is competent).
by cstrahan
3/12/2025 at 3:38:54 PM
Agreed —seeinglogic's article made me think of a 3rd option:
1. Sorta long functional chain where the type changes partway through 2. Use temp variables 3. (New option) Use comments
(Here's funcA from seeinglogic's article, but I added 3 comments)
function funcC(graph) {
return
// target node
graph.nodes(`node[name = ${name}]`)
// neighbor nodes
.connected()
.nodes()
// visible names
.not('.hidden')
.data('name');
}
Compare to funcB which uses temp variables: function funcB(graph) {
const targetNode = graph.nodes(`node[name = ${name}]`)
const neighborNodes = targetNode.connected().nodes();
const visibleNames = neighborNodes.not('.hidden').data('name')
return visibleNames;
}
For me the commented version is easier to read and audit and it also feels safer for some reason, but I'm not how subjective that is.
by p2edwards
3/12/2025 at 8:03:09 PM
Funny, I see the need to add comments as an indicator that it's time to introduce chunking of some sort--arguably it already has been.In this case, I'd lean towards intermediate variables, but sometimes I'll use functions or methods to group things.
I prefer function/method/variable names over comments because the are actual first class parts of the code. In my experience, people are a bit more likely to update them when they stop being true. YMMV.
by daotoad
3/11/2025 at 9:03:35 PM
The dig on chains of map/reduce/filter was listed as a "Halstead Complexity Takeaway", and seemed to come out of the blue, unjustified by any of the points made about Halstead complexity. In fact in your later funcA vs. funcB example, funcB would seem to have higher Halstead complexity due to its additional variables (depending on whether they count as additional "operands" or not). In general, long chains of functions seem like they'd have lower Halstead complexity.The "anti-functional Tourette's" comment was partly a response to how completely random and unjustified it seemed in that part of the article, and also that this feels like a very common gut reaction to functional programming from people who aren't really willing to give it a try. I'm not only arguing directly against you here, but that attitude at large.
Your funcA vs. funcB example doesn't strike me as "functional" at all. No functions are even passed as arguments. That "fluent" style of long chains has been around in OO languages for a while, independent of functional programming (e.g. see d3.js*, which is definitely not the oldest). Sure, breaking long "fluent" chains up with intermediate variables can sometimes help readability. I just don't really get how any of this is the fault of functional programming.
I think part of the reason funcB seems so much more readable is that neither function's name explains what it's trying to do, so you go from 0 useful names to 3. If the function was called "getNamesOfVisibleNeighbors" it'd already close the readability gap a lot. Of course if it were called that, it'd be more clear that it might be just trying to do too much at once.
I view the "fluent" style as essentially embedding a DSL inside the host language. How readable it is depends a lot on how clear the DSL itself is. Your examples benefit from additional explanation partly because the DSL just seems rather inscrutable and idiosyncratic. Is it really clear what ".data()" is supposed to do? Sure, you can learn it, but you're learning an idiosyncrasy of that one library, not an agreed-upon language. And why do we need ".nodes()" after ".connected()"? What else can be connected to a node in a graph other than other nodes? Why do you need to repeat the word "node" in a string inside "graph.nodes()"? Why does a function with the plural "nodes" get assigned to a singular variable? As an example of how confusing this DSL is, you've claimed to find "visibleNames", but it looks to me like you've actually found the names of visible neighborNodes. It's not the names that are not(.hidden), it's the nodes, right? Consider this:
function getVisibleNeighborNames(graph) {
return graph
.nodeByName(name)
.connectedNodes()
.filter(node => !node.isHidden)
.map(node => node.name)
}
Note how much clearer ".filter(node => !node.isHidden)" is than ".not('.hidden')", and ".map(node => node.name)" versus ".data('name')". It's much harder to get confused about whether it's the node or the name that's hidden, etc.Getting the DSL right is really hard, which only increases the benefit of using things like "map" and "filter" which everyone immediately understands, and which have no extrinsic complexity at all.
You could argue that it's somehow "invalid" to change the DSL, but my point is that if you're using the wrong tool for the job to begin with, then any further discussion of readability is in some sense moot. If you're doing a lot of logic on graphs, you should be dealing with a graph representation, not CSS classes and HTML attributes. Then the long chains are not an issue at all, because they read like a DSL in the actual domain you're working in.
*Sidenote: I hate d3's standard style, for some of the same reasons you mention, but mainly because "fluent" chains should never be mutating their operand.
by feoren
3/11/2025 at 10:19:07 PM
Just want to add that I both agree with you and parent. Your examples are readable and I see no issues there.It might be a language thing as well. In Python often people take list-comprehensions too far and it becomes an undecipherable mess of nested iterators, casts and lists.
There are always exceptions :)
by climb_stealth
3/12/2025 at 8:16:45 PM
I read the OP as disliking the Python style, which I agree with.However the GPs example of map filter distinct I find lovely.
Mind you, I learned to program is a functional language so I probably have a different perspective to most.
by disgruntledphd2
3/11/2025 at 11:48:26 PM
> mainly because "fluent" chains should never be mutating their operand.I see this quite often with builders, actually, and I don't mind it so much there.
FooBuilder()
.setBar(bar)
.setBaz(baz)
.setQux(qux)
.build()
by vitus
3/12/2025 at 12:25:11 AM
everytime i see this i just would prefer a variadic function lolby wegfawefgawefg
3/12/2025 at 1:07:09 AM
If your language supports named arguments, sure, but that's not always a given.by vitus
3/15/2025 at 12:02:36 AM
i dont buy it. this pattern was common in rust and then fell out of favor.in c or go if you dont like variadics just pass in a params struct.
its just a fad.
by wegfawefgawefg
3/16/2025 at 4:32:05 AM
> in c or go if you dont like variadics just pass in a params struct.Which, again, is fine if your language supports named args for your params struct.
C++ didn't have this until C++20, despite C having it for decades prior.
Java still doesn't have this.
If your language doesn't have named args or designated initializers or whatever it calls them, then what, perform your function calls with argument comments of the form f(/*foo=*/1, /*bar=*/2, /*baz=*/true)? That's error-prone even in the best-case scenario.
by vitus
3/11/2025 at 9:41:30 PM
o_node := graph.GetNodeByName(name)
var ret []string
for _, node := range o_node.connectedNodes() {
if !node.isHidden {
ret = append(ret, node.name)
}
}
return ret
by desumeku
3/11/2025 at 11:05:33 PM
There is just no way that reasonable people consider this to be clearer. One certainly might be more familiar with this approach, but it is less clear by a long shot.You've added a temp variable for the result, manual appending to that temp variable (which introduces a performance regression from having to periodically grow the array), loop variables, unused variables, multiple layers of nesting, and conditional logic. And the logic itself is no longer conceptually linear.
by stouset
3/11/2025 at 11:16:46 PM
Everything you said is true for both of our programs, the only difference is whether or not it's hidden behind function calls you can't see and don't have access to.You don't really think that functional languages aren't appending things, using temp vars, and using conditional logic behind the scenes, do you? What do you think ".filter(node => !node.isHidden)" does? It's nothing but a for loop and a conditional by another name and wrapped in an awkward, unwieldy package.
>which introduces a performance regression from having to periodically grow the array
This is simply ridiculous, do you just believe that the magic of Lisp/FP allows it to pluck the target variables out of the sky in perfectly-sized packages with zero allocation or overhead?
by desumeku
3/12/2025 at 12:30:58 AM
> the only difference is whether or not it's hidden behind function calls you can't see and don't have access to.You "can't see and don't have access to" `if`, `range`, or `append` but somehow you don't find this a problem at all. I wonder why not?
> You don't really think that functional languages aren't appending things, using temp vars, and using conditional logic behind the scenes, do you?
By this metric all languages that compile down to machine instructions are equivalent. After all, it winds up in the same registers and a bunch of CMP, MOV, JMP, and so on.
`.distinct()` could sort the result and look for consecutive entries, it could build up a set internally, it could use a hashmap, or any one of a million other approaches. It can even probe the size of the array to pick the performance-optimal approach. I don't have to care.
> [".filter(node => !node.isHidden)" is] nothing but a for loop and a conditional by another name and wrapped in an awkward, unwieldy package.
This is honestly an absurd take. I truly have no other words for it. map, filter, and friends are quite literally some of the clearest and most ergonomic abstractions ever devised.
by stouset
3/12/2025 at 10:48:34 AM
Speaking of filters and clear ergonomic abstractions, if you like programming languages with keyword pairs like if/fi, for/rof, while/elihw, goto/otog, you will LOVE the cabkwards covabulary of cepstral quefrency alanysis, invented in 1963 by B. P. Bogert, M. J. Healy, and J. W. Tukey:cepstrum: inverse spectrum
lifter: inverse filter
saphe: inverse phase
quefrency alanysis: inverse frequency analysis
gisnal orpcessing: inverse signal processing
by DonHopkins
3/12/2025 at 3:29:26 AM
> it could build up a set internally, it could use a hashmap, or any one of a million other approaches. It can even probe the size of the array to pick the performance-optimal approach. I don't have to care.Well, this is probably why functional programming doesn't see a lot of real use in production environments. Usually, you actually do have to care. Talk about noticing a performance regression because I was simply appending to an array. You have no idea what performance regressions are happening in ANY line of FP code, and on top of that, most FP languages are dead-set on "immutability" which simply means creating copies of objects wherever you possibly can... (instead of thinking about when it makes sense and how to be performant about it)
by desumeku
3/12/2025 at 11:28:51 AM
Your assumption that filter/map/reduce is necessarily slower than a carefully handcrafted loop is wrong, though. Rust supports these features as well and the performance is equivalent.Also countless real-world production environments run on Python, Ruby, JS etc, all of which are significantly slower than a compiled FP program using filter & map.
> FP languages are dead-set on "immutability" which simply means creating copies of objects
Incorrect. The compiler can make it mutable for better performance, and that gives you the best of both worlds: immutability where a fallible human is involved, and mutability where it matters.
by alpaca128
3/12/2025 at 2:09:02 PM
> The compiler can make it mutable for better performanceWell, we already know that no pure FP language can match the performance of a dirty normal imperative language, except for Common Lisp (which I am happy to hear an explanation for how it manages to be much faster than the rest, maybe it's due to the for loops?). And another comment here already mentioned how those "significantly slower" scripting languages have a healthy dose of FP constructs -- which are normally considered anti-patterns, for good reason. The only language that competes in speed in Rust, which just so happens to let you have fast FP abstractions so long as you manually manage every piece of memory and its lifetime, constantly negotiating with the compiler in the process, thereby giving up any of the convenience benefits you actually get from FP.
by desumeku
3/12/2025 at 7:28:14 PM
You complain about not having control of how the `filter` works under the hood but are happy to give the language control over memory management. How much abstraction is too much abstraction? Where do you draw the line?by maleldil
3/12/2025 at 7:59:18 AM
I consider an abstraction to be a good abstraction if I don't need to care for its internal workings all the time - whether that's some build step in the CI, or the usage of data structures from simple strings to advanced Cuckoo filters and beyond. Even Python uses a Bloom filter with operations on strings internally AFAIK. Correctness and maintainability trumps performance most of the time, and map, filter and immutability are building blocks to achieve just that. Those constructs won't prevent you from looking deeper and doing something different if you really have to (and know what you're doing), they just acknowledge that you'll probably don't need to do that.by whilenot-dev
3/12/2025 at 2:18:08 PM
> Correctness and maintainability trumps performanceGreat, please make all the software even slower than it already is. I am overjoyed to have to purchase several new laptops a decade because they become e-waste purely due to the degradation of software performance. It is beyond ridiculous that to FP programmers daring to mutate variables or fine-tune a for loop is an exceptional scenario that you don't do "unless you have to" and which requires "knowing what you're doing". Do you know what you're doing? How can you be a software engineer and think that for loops are too difficult of a construct, and that you need something higher-level and more abstract to feel safe? It's insane. Utterly insane. Perhaps even the root of all evil, Code Golf manifested as religion.
by desumeku
3/13/2025 at 10:36:51 PM
> Great, please make all the software even slower than it already is.It is more or less trivial to emit essentially optimal autovectorized and inlined machine code from functional iterators.
Rust does this, for example.
by stouset
3/12/2025 at 7:57:18 AM
>Well, this is probably why functional programming doesn't see a lot of real use in production environmentsThe usual map/filter/reduce is everywhere in production. Python, java, js, ruby, c#...
You could even argue that lack of generics hurt Go's popularity for a while precisely for that usecase.
by kace91
3/12/2025 at 1:48:53 PM
> by this metricFalse equivalence. You're saying that the statement "both for.. append and .map() executing the _same steps_ in the _same order_ are the same" is equivalent to saying that "two statements being composed of cmp,jmp, etc (in totally different ways) are the same" That is a dishonest argument.
> Distinct could sort and look for consecutive, it could use a hashmap
People love happy theories like this, but find me one major implementation that does this. For example, here is the distinct() implementation in the most abstraction-happy language that I know - C#
https://github.com/dotnet/runtime/blob/main/src%2Flibraries%...
It unconditionally uses a hashset regardless of input.
Edit: found an example which does different things depending on input
https://github.com/php/php-src/blob/master/ext/standard/arra...
This does the usual hashset based approach only if the array has strings. Otherwise, it gets the comparator and does a sort | uniq. So, you get a needless O(nlogn), without having a way to distinct() by say, an `id` field in your objects. Very ergonomic...
On the other hand...
seen := map[string]bool{}
uniq := []string{}
for _, s := range my_strings {
if !seen[s] {
uniq = append(uniq, s)
seen[s] = true;
}
}
Let us say you want to refactor and store a struct instead of just a string. The code would change to... seen_ids := map[string]bool{}
uniq := []MyObject{}
for _, o := range my_objects {
if !seen_ids[o.id] {
uniq = append(uniq, o)
seen_ids[o.id] = true;
}
}
Visually, it is basically the same, with clear visual messaging of what has changed. And as a bonus, it isn't incurring a random performance degradation.Edit 2: An SO question for how to do array_unique for objects in php. Some very ergonomic choices there... https://stackoverflow.com/questions/2426557/array-unique-for...
by porridgeraisin
3/13/2025 at 11:01:07 PM
I'm not sure reaching for PHP to dunk on functional languages is the win you think it is?> Visually, it is basically the same, with clear visual messaging of what has changed.
In order to do this you had to make edits to all but a single line of logic. Literally only one line in your implementation didn't change.
Compare, with Ruby:
# strings
uniq = strings.uniq()
# structs, if they are considered equal based on the
# field in question (strings are just structs and they
# implement equality, so of course this is the same)
uniq = structs.uniq()
# structs, if you want to unique by some mechanism
# other than natural equality
let uniq = structs.uniq(&:id)
With Rust: # strings
let uniq = strings.unique();
# structs, if they are considered equal based on the
# field in question (strings are just structs and they
# implement equality, so of course this is the same)
let uniq = structs.unique();
# structs, if you want to unique by some mechanism
# other than natural equality
let uniq = structs.unique_by(|s| s.id);
You cannot tell me with a straight face that your version is clearer. You'll note that both languages have essentially the exact same code, which is to say: nearly none at all.
by stouset
3/14/2025 at 9:27:48 AM
> I'm not sure reaching for PHPMy initial comment was a response to your comment
> Distinct could sort and look for consecutive, it could use a hashmap. It can even probe the size of the array to pick the performance-optimal approach. I don't have to care.
which says that you just use an abstraction and it transparently "does the right thing". The C# example showed how abstractions actually don't do that, and instead simply provide a lowest common denominator implementation. The rust examples you used also uses a hashmap unconditionally. The PHP example was showing how when abstractions attempt to do that it ends up even worse - when you move from strings to structs, you get a slowdown.
In practice, different strategies are always implemented as different functions (see python `moreitertools` `unique_justseen` and `unique_everseen`), at which point its no longer an abstraction (which by definition serves multiple purposes) and it just becomes a matter of whether or not the set of disparate helper functions is written by you, the standard library, or a third party. In rust, you would do `vec.sort()`, `vec.dedup()` for one strategy, and call `v.into_iter().unique().collect()` for the other strategy. That is not an "abstraction" [which achieves what you claimed they do].
by porridgeraisin
3/12/2025 at 7:36:31 PM
> It unconditionally uses a hashset regardless of input.This follows what I like to call macro optimization. I don't care if a hash set is slightly slower for small inputs. Small inputs are negligibly fast anyway. The hash set solution just works for almost any dataset. Either it's too small to give a crap, midsized and hash set is best, or huge and hash set is still pretty much best.
That's why we default to the best time/space complexity. Once in a blue moon someone might have an obscure usecase where micro optimizing for small datasets makes sense somehow, for the vast majority of use cases this solution is best.
by sfn42
3/12/2025 at 3:47:56 AM
> Everything you said is true for both of our programs, the only difference is whether or not it's hidden behind function calls you can't see and don't have access to.This is a key difference between imperative programming and other paradigms.
> You don't really think that functional languages aren't appending things, using temp vars, and using conditional logic behind the scenes, do you?
A key concept in a FP approach is Referential Transparency[0]. Here, this concept is relevant in that however FP constructs do what they do "under the hood" is immaterial to collaborators. All that matters is if, for some function/method `f(x)`, it is given the same value for `x`, `f(x)` will produce the same result without observable side effects.
> What do you think ".filter(node => !node.isHidden)" does?
Depending on the language, apply a predicate to a value in a container, which could be a "traditional" collection (List, Set, etc.), an optional type (cardinality of 0 or 1), a future, an I/O operation, a ...
> It's nothing but a for loop and a conditional by another name and wrapped in an awkward, unwieldy package.
There is something to be said for the value of using appropriate abstractions. If not, then we would still be writing COBOL.
by AdieuToLogic
3/11/2025 at 11:42:41 PM
> You don't really think that functional languages aren't appending things, using temp vars, and using conditional logic behind the scenes, do you? What do you think ".filter(node => !node.isHidden)" does? It's nothing but a for loop and a conditional by another name and wrapped in an awkward, unwieldy package.But the whole point of higher-level languages is that you don't have to think about what's going on behind the scenes, and can focus on expressing intent while worrying less about implementation. Just because a HLL is eventually compiled into assembler, and so the assembler expresses everything the HLL did, doesn't mean the HLL and assembler are equally readable.
(And I think that your parent's point is that "awkward, unwieldy package" is a judgment call, rather than an objective evaluation, based, probably, on familiarity and experience—it certainly doesn't look awkward or unwiely to me, though I disagree with some of the other aesthetic judgments made by your parent.)
by JadeNB
3/12/2025 at 1:42:59 PM
> performance regressionWhat? Golang append()s also periodically grow the slice.
> Conditional logic
it's just a single if, really, the same thing is there in your filter()
> Multiple layers of nesting
2... You're talking it up like it's a pyramid of hell. For what it's worth, I've seen way way more nesting in usual FP-style code, especially with formatting tools doing
func(
args
)
For longer elements of the function chain.
by porridgeraisin
3/13/2025 at 2:44:02 AM
> What? Golang append()s also periodically grow the slice.If you already know the size of the result (there are no filtering operations), the functional approach can trivially allocate the resulting array to already have the correct capacity. This happens with zero user intervention.
IIRC the Rust optimizer basically emits more or less optimal machine code (including SIMD) for most forms of iteration.
by stouset
3/13/2025 at 6:55:56 AM
We are talking about making an array with unique elements here. You cannot know the correct capacity for that without overallocating.If overallocating is indeed OK for your usecase, then you can do so yourself
uniq := make([]MyObject, 0, len(my_objects))
by porridgeraisin
3/13/2025 at 3:34:59 PM
I was talking more in the general case.Yes, you can always just write more and more and more code to fix these things. Or you could just… not write more code and still get optimal performance.
by stouset
3/13/2025 at 5:43:12 PM
> just... Not write more codeBut the abstractions don't really adapt themselves to be performant in each usecase the way you imagine. I gave an example of c# distinct() above. Sure, they can. But do they? No. They only save anything at all for trivial usecases, where the imperative code is also obvious to parse.
by porridgeraisin
3/12/2025 at 10:38:23 AM
o_god!by DonHopkins
3/11/2025 at 8:46:13 PM
>For me, roughly 5 calls in a chain is where things begin to become harder to read, which is the length of the example I used.This isn't just about readability. Chaining or FP is structurally more sound. It is the more proper way to code from a architectural and structural pattern perspective.
given an array of numbers
1. I want to add 5 to all numbers
2. I want to convert to string
3. I want to concat hello
4. I want to create a reduced comma seperated string
5. I want to capitalize all letters in the string.
This is what a for loop would look like: // assume x is the array
acc = ""
for(var i = 0, i < x.length; x++) {
value = x[i] + 5
value += 5
stringValue = str(value).concat(hello)
acc += stringValue + ","
}
for (var i = 0, i < acc.length; i++) {
acc[i] = capitalLetter(acc[i])
}
FP: addFive(x) = [i + 5 for i in x]
toString(x) = [str(i) for i in x]
concatHello = [i + "hello" for i in x]
reduceStrings(x) = reduce((i, acc) = acc + "," + i, x)
capitalize(x) = ([capitalLetter(i) for i in x]).toString()
You have 5 steps. With FP all 5 steps are reuseable. With Procedural it is not.Mind you that I know you're thinking about chaining. Chaining is eqivalent to inlining multiple operations together. So for example in that case
x.map(...).map(...).map(...).reduce(...).map(...)
//can be made into
addFive(x) = x.map(...)
toString(x)= x.map(...)
...
By nature functional is modular so such syntax can easily be extracted into modules with each module given a name. The procedural code cannot do this. It is structurally unsound and tightly coupled.It's not about going overboard here. The FP simply needs to be formatted to be readable, but it is the MORE proper way to code to make your code modular general and decoupled.
by ninetyninenine
3/11/2025 at 9:41:41 PM
you have this backwards: reusing code couples the code. copy+paste uncouples codeif you have two functions, they're not coupled. you change one, the other stays as-is
if you refactor it so that they both call a third function, they're now coupled. you can't change the part they have in common without either changing both, or uncoupling them by duplicating the code
(you often want that coupling, if it lines up with the semantics)
by harrison_clarke
3/12/2025 at 12:54:46 AM
I meant code within the snippet is tightly coupled. You can't just cut the for loop in half and reuse half the logic.by ninetyninenine
3/11/2025 at 6:38:28 PM
Your example is a conceptually simple filter on a single list of items. But once the chain grows too long, the conditions become too complex, and there are too many lists/variables involved, it becomes impossible understand everything at once.In a procedural loop, you can assign an intermediate result to a variable. By giving it a name, you can forget the processing you have done so far and focus on the next steps.
by jltsiren
3/11/2025 at 6:41:14 PM
You don't ever need to "understand everything at once". You can read each stanza linearly. The for loop style is the approach where everything often needs to be understood all at once since the logic is interspersed throughout the entire body.by stouset
3/11/2025 at 8:02:32 PM
This. I teach this with Pandas (and Polars) all the time. You don't really care about the intermediate values. You build up the chain operation by operation (validating that it works). At the end you have a recipe for processing the data.Most professional Pandas users realize that working with chains makes their lives much easier.
By the way, debugging chains isn't hard. I have a chapter in my book that shows you how to do it.
by __mharrison__
3/11/2025 at 6:54:53 PM
In the example above, you first have a list of books. Then you filter it down to books with >1000 pages. Then you map it to authors of books with >1000 pages. Then you collapse it to distinct authors of books with >1000 pages. Every step in the chain adds further complexity to the description of the things you have, until it exceeds the capacity of your working memory. Then you can no longer reason about it.The standard approach to complexity like that is to invent useful concepts and give them descriptive names. Then you can reason about the concepts themselves, without having to consider the steps you used to reach them.
by jltsiren
3/11/2025 at 7:21:24 PM
Folks who are familiar with chaining don't think about it in the way that you've presented. If you're familiar, it's more like:Filter to the books with >1000 pages
Then their authors.
Finally, distinguish those authors.
If you're familiar, you don't mentally represent each link in the chain as the totality of everything that came before it _plus_ whatever operation you're doing now. You consider each link in the chain in isolation, as its inputs are the prior link and its outputs will be used in the next link. Giving a name to each one of those links in the chain isn't always necessary, and depending on how trivial the operations are, can really hurt readability.
I think its very much a personal preference.
by lelandbatey
3/11/2025 at 7:37:59 PM
The problem with that is that it's all implicit. If the steps are sufficiently complex and if you don't already know what the code is doing, you don't always have a clear mental image of what the intermediate state after each step is supposed to represent. And with a chained syntax like that, you don't have an option to give the intermediate state an explicit name. A name that could help the reader understand what is going on.You don't have to give a name to every intermediate state, just like you don't have to comment every single line of code. But sometimes the names and comments do improve readability.
by jltsiren
3/11/2025 at 9:02:54 PM
That's a question of code organization, one method I find helpful is writing a long chain and then breaking it up into clear functions. E.g. var longBooks = books.filter(book => book.pageCount > 1000)
var authors = longBooks.map(book => book.author)
var distinctAuthors = authors.distinct()
could become (in a different language) books
|> Books.filter_by_length(min: 1000)
|> Authors.from_books()
|> Enum.distinct()
and now each step is named and reusable. This example isn't the best, but it can be quite helpful when you have large map() and filter() logic blocks.
by solid_fuel
3/11/2025 at 9:04:15 PM
I have a guideline where I tend to put a name on a result if and only if it changes the type of the data compared to the previous step. It works well.by yxhuvud
3/11/2025 at 7:39:33 PM
So just do that then in the cases where you think it improves clarity? It's not like you can't assign names in the functional style if you need to.by stouset
3/11/2025 at 7:59:25 PM
The problem is the "you" in question is not always able to. When "you" write code it makes sense and so you don't need to assign many names. The you in six months will want more names, and in 6 years that will be different again (how many depends - if this code is changed often then you know it much better than if it has been stable). The worse case will be after you "get hit by a bus" and the "you" in question is some poor person who has never seen this code before.by bluGill
3/11/2025 at 8:45:34 PM
Unlike the procedural approach, every step in a functional chain is wholly isolated and independent from the others. It is strictly easier to split this style of code up into two halves and name them than it is to disentangle procedural equivalents.I have quite literally zero times in my ~25 year career had to deal with some sort of completely inscrutable chain of functional calls on iterators. Zero. I am entirely convinced that the people arguing against this style have never actually worked in a project where people used this style. It's okay! The first time I saw these things I, too, was terribly confused and skeptical.
by stouset
3/12/2025 at 12:30:19 PM
I will admit to not having written any significant functional code. However the poster child for functional programming always seems to be small programs (xmonad is the largest one I can think of, and the procedural counterparts are not that big either. Of course there is a lot of code out there that nobody can talk about). Thus I have to conclude the question of how that style scales to really large programs remains open.That said, you didn't address my comment at all. It might be easier, but that doesn't mean it is easy to figure out what that long chain is really done - all too often the algorithm names don't tell you what you are really trying to accomplish in my experience.
by bluGill
3/12/2025 at 1:46:33 PM
Ahh, it gets really interesting when you read code that does have named variables… and they’re misleading.A strength of functional idioms is that they expose the structure of the code in a way that a name - even a well chosen name - can only hope to achieve. Often, succinctly and comprehensively. At that point you stop caring so much about variable names. They’re still there but you need them less
by diatone
3/11/2025 at 7:38:59 PM
You have literally just described the set of objects asked for: the unique authors of the books with more than 1,000 pages. I don't understand how you expect to get any simpler than that. The functional style isn't even requiring you to describe how to accomplish it, it almost verbatim simply describes the answer you're trying to get.If your entire objection is that you might want intermediate-named variables… you can just do that?
var longBooks = books.filter(book => book.pageCount > 1000)
var authors = longBooks.map(book => book.author)
var distinctAuthors = authors.distinct()
For short chains (95%+ of cases), this is far more mental overhead. For the remaining cases, you can just name the parts? I'm just completely failing to see your problem here.
by stouset
3/11/2025 at 7:56:57 PM
The problem is that it's easy to overdo it. When you are writing the code, you already know what it's supposed to do, and adding a few more things to the chain is convenient and attractive. But when you are reading unfamiliar code, you often wish that the author was more explicit with their code. Not just with what the code is actually doing, but what it's trying to do and what are the key waypoints to get there.With procedural code, it's widely accepted that you should not do too many things in a single statement. But in functional code, the entire chain is a single statement. There are no natural breakpoints where the reader could expect to find justifications for the code.
by jltsiren
3/11/2025 at 11:05:47 PM
> But in functional code, the entire chain is a single statement. There are no natural breakpoints where the reader could expect to find justifications for the code.How are we deciding what's "functional code", here? Because functional languages also provide means like `let` and `where` bindings to break up statements. The example might in pseudo-Haskell be broken up like
distinctAuthors = distinct authors
where
authors = map (\book -> book.author) longBooks
longBooks = filter (\book -> book.pageCount > 1000) books
IMO the code here is also simple enough that I don't see it needing much in the way of comments, but it is also possible and common to intersperse comments in the dot style, e.g. distinctAuthors = books // TODO: Where does this collection come from anyway?
// books are officially considered long if they're over 1000 pages, c.f. the Council of Chalcedon (451)
.filter(book => book.pageCount > 1000)
// All books have exactly one author for some reason. Why? Shouldn't this be a flatmap or something?
.map(book => book.author)
// We obviously actually want a set[author] here, rather than a pruned list[author],
// but in this imaginary DinkyLang we'd have to implement that as map[author, null]
// and that's just too annoying to deal with
.distinct()
by syklemil
3/12/2025 at 12:26:28 AM
If you're used to it, then it doesn't read like a single statement, even though technically it is. You put each call of the chain on its own line and it feels like reading the lines of regular imperative code. Except better because I can be sure that each line strictly only uses the result of the previous line, not two or three lines before so the logic flows nicely linearly.by bonoboTP
3/11/2025 at 10:35:26 PM
> But in functional code, the entire chain is a single statementNot necessarily. You can use intermediate variables when necessary.
by whstl
3/11/2025 at 8:40:38 PM
> The problem is that it's easy to overdo it.Welcome to all features of every programming language?
Sacrificing readability, optimization, and simplicity for the 95% case because some un-principled developers might overdo it in the 5% case (when the cost of fixing it is trivially just inserting variable assignments) is… not a good trade-off.
by stouset
3/11/2025 at 9:31:02 PM
5% is common enough that you'll encounter it almost every time you read code. And fixing it is not easy, because you first need to understand the code before you can add useful variable names.Besides, programming language evolution is mostly driven by the fact that everyone is lazy and unprincipled at least occasionally. If you need to be disciplined to avoid footguns, you'll trigger them sooner or later.
by jltsiren
3/11/2025 at 9:32:50 PM
The cost of this "footgun" is basically zero. Every step in a functional pipeline is isolated and wholly independent. If you want to split such a pipeline in two, doing so is trivial.by stouset
3/13/2025 at 7:17:49 AM
It's not clear what point you are trying to make because so far you are describing problems common to all programming languages.by vendiddy
3/11/2025 at 10:34:54 PM
5% is also low enough that you can just use another technique for the exceptions.by whstl
3/12/2025 at 5:50:45 AM
Your example shows that it is possible to give NAMES to the intermediate results of a long chain.Giving names to things makes it easier to understand the intention of the programmer.
And that also allows you to create a TREE of dataflow-code not just a CHAIN. For instance 'longBooks' could be used as the starting point of multiple different chains.
It gets complicated at some point but I think other approaches result in code that is even harder to understand.
by galaxyLogic
3/11/2025 at 8:03:10 PM
It's also harder to write and debug with the intermediate steps.by __mharrison__
3/11/2025 at 10:10:54 PM
How so? The states of the intermediate steps are logically and easily exposed in a debugger. You can also easily set conditional breakpoints relative to the intermediate states.I know that intermediate states are generally easier to comprehend, because I never have to explain them in code reviews. To avoid having to explain chains to others, I end up having to add descriptive comments to the intermediate steps, far exceeding the number of characters the descriptive intermediate variables would take. That's why I avoid them, or break them up: time spent in code reviews has proven to me that people have trouble with chains.
by nomel
3/11/2025 at 10:40:44 PM
Build up and debug the chain as you work in an environment like Jupyter. No need to create variables. Just run the code and verify that the current step works. Then, proceed to the next. Then, put the chain in a function. If you want to be nice, put a .loc as the first step to explicitly list all of the input columns. Drop another .loc as the last step to validate the output columns. (This also serves as a test and documentation to future you about what needs to come in and out.) Create a simple unit test with a sample of the data if you desire.I've found that the constraint of thinking in chains forces me to think of the recipe that I need for my data. Of course, not everything can be done in a stepwise manner (.pipe helps with that), but often, this constraint forces you to think about what you are doing.
Every good Pandas user I know uses it this way. I've taught hundreds more. Generally, it feels weird at first (kind of like whitespace in Python), but after a day, you get used to it.
Do you store intermediate results of SQL?
by __mharrison__
3/12/2025 at 12:51:34 AM
> Build up and debug the chain as you work in an environment like Jupyter.> Just run the code and verify that the current step works. Then, proceed to the next.
Yes, it's not debuggable/"viewable" without cut/paste/commenting out lines, once it's constructed.
by nomel
3/12/2025 at 4:13:26 AM
If it is breakpoints you are concerned about, you can set a breakpoint on a method in the chain and inspect `self`.by __mharrison__
3/12/2025 at 6:19:27 PM
Most languages don't expose the internal of map to set a breakpoint, so you're left with individual entities. But yes, there are tricks you can use to make it work, although most require more complex conditional/sequential breakpoints. In your method breakpoint example, you would need to set a chained breakpoint, as in "don't break until this other breakpoint above the chain has been hit", otherwise the breakpoint in the method won't be "spatially" relevant to the code you're debugging.by nomel
3/12/2025 at 12:33:09 AM
Each predicate is a separate scope. How is the complexity additive? If you really have to you can simply be just as specific in your predicate naming as you would in a for loop. var authorsOfLongBooks = books
.filter(book => book.pageCount > 1000)
.map(longBooks => longBooks.author)
.distinct()
by jayd16
3/11/2025 at 6:59:47 PM
That's only true for casual reviewing and writing.When you're actually analyzing a bug, or need to add a new feature to the code... Then you'll have to keep the whole thing in your mind. No way around it
It gets extra annoying when people have complex maps, reduces, flat maps all chained after the next, and each step moved into a named function.
HF constantly jumping around trying to rationalize why something happens with such code...
It looks good on first glance, but it inevitably becomes a dumpster fire as soon as you need to actually interact with the code.
by ffsm8
3/11/2025 at 7:14:39 PM
In a practical example you'd create a named intermediate type which becomes a new base for reasoning. Once you convinced yourself that the first part of the chain responsible for creating that type (or a collection of it) is correct, you can forget it and free up working memory to move on to the next part. The pure nature of the steps also makes them trivially testable as you can just call them individually with easy to construct values.by reubenmorais
3/11/2025 at 7:46:40 PM
If you assign an intermediate result to a variable in a procedural loop, you can also assign intermediate results of parts of this chain to variables.by kccqzy
3/11/2025 at 6:50:17 PM
SELECT DISTINCT author FROM books WHERE pageCount > 1000;by titzer
3/12/2025 at 3:24:21 AM
In fairness, if this was in a relational data store, the same code as above would probably look more like...SELECT DISTINCT authors.some_field FROM books JOIN authors ON books.author_id = authors.author_id WHERE books.pageCount > 1000
And if you wanted to grab the entire authors record (like the code does) you'd probably need some more complexity in there:
SELECT * FROM authors WHERE author_id IN ( SELECT DISTINCT authors.author_id FROM books JOIN authors ON books.author_id = authors.author_id WHERE books.pageCount > 1000 )
by 101011
3/12/2025 at 4:15:55 AM
The last one is better as:SELECT * FROM authors WHERE author_id IN (SELECT author_id FROM books WHERE pageCount > 1000);
But I think you're missing the point. The functional/procedural style of writing is sequentialized and potentially slow. It's not transactional, doesn't handle partial failure, isn't parallelizable (without heavy lifting from the language--maybe LINQ can do this? but definitely not in Java).
With SQL, you push the entire query down into the database engine and expose it to the query optimizer. And SQL is actually supported by many, many systems. And it's what people have been writing for 40+ years.
by titzer
3/12/2025 at 4:50:45 PM
agreed on the revised SQL!But I don't think I missed the point, the original text talks about measuring complexity as a function of operators, operands, and nested code. The true one to one mapping is more complex than the original comment I replied to
by 101011
3/11/2025 at 10:29:59 PM
Scrolled to find the SQL. Such an elegant, powerful language. Really happy I chose SQLite for my game/project.by YesBox
3/12/2025 at 1:14:13 PM
Just add strong types, with verification at something-like-compile-time, and you'll have a sane language.by pif
3/11/2025 at 9:11:09 PM
This is 5 times more readable than FP example above for the same computation. The FP example uses variable book(s) five times, where using it once was sufficient for SQL. Perhaps FP languages could have learned something from SQL...by beryilma
3/12/2025 at 7:50:23 AM
Now modify the SQL to support books having multiple authors. (In the FP example, you would just change map to flatMap.)by xigoi
3/11/2025 at 11:47:07 PM
Notably this example is declarative, the original is functional, and neither is imperative.by odyssey7
3/11/2025 at 8:04:15 PM
Folks don't seem to have a problem when SQL does it. Only when code like Pandas does it...by __mharrison__
3/11/2025 at 11:39:51 PM
Hi Matt! I've observed this phenomenon as well.When the SQL and Pandas examples are isomorphic except for shallow syntactic differences, the root cause of the complaint must either be:
* that the judgment was emotional rather than substantive * or that the syntactic differences (dots and parens) actually matter
by mont_tag
3/12/2025 at 4:14:29 AM
Folks really like their intermediate dataframes... for "debugging".by __mharrison__
3/12/2025 at 9:26:16 AM
While I think your example is fine, I think the complaint was more about very long chains. Personally, I like to break them up to give intermediate results names, kinda like using variable names as comments: var longBooks = books.filter(book => book.pageCount > 1000)
var authorsOfLongBooks = longBooks.map(book => book.author).distinct()
by RedNifre
3/11/2025 at 9:51:02 PM
I like the SQL solutions people posted. But what about this one in Prolog? ?- setof(Author, Book^Pages^(book_author(Book, Author), book_pages(Book, Pages), Pages > 1000), Authors).
Depending on the structure of the Prolog database, it could be shorter: ?- setof(Author, Pages^(book(_, Author, Pages), Pages > 1000), Authors).
by matejn
3/11/2025 at 6:51:30 PM
That's not a long chain. It doesn't even have a reduce, try nesting a few reducers and see how you like it.by dsego
3/11/2025 at 7:36:28 PM
What is "long"?by aaronbrethorst
3/12/2025 at 7:30:40 AM
Are you really confused when people don't think 3 operations constitutes "long"? I would guess anyone with half a brain would agree 3 operations is not long, maybe 5 or 6 and you will have many people agreeing, and above that most.by brabel
3/12/2025 at 11:27:50 AM
Here's an abomination of my own design in Rust for example: for (index, node) in nodes
.expect("Error: No blocks for the Body")
.children()
.expect("Error: blocks node has no children")
.nodes()
.iter()
.enumerate()
{
let block = Block::new(node, index);
self.blocks.push(block);
}
by aquariusDue
3/12/2025 at 7:32:46 PM
Why the for loop instead of mapping the enumerate iterator into Block::new and collect::Vec?by maleldil
3/13/2025 at 9:33:47 AM
I lack a good answer other than it didn't occur to me, now I have some code to refactor. Thanks!by aquariusDue
3/13/2025 at 3:37:07 PM
FWIW, I've had to write similar code when there's some complex "folding" operation. I use map/filter a lot, but fold/reduce/accumulate always seemed harder to understand than a for loop. I also prefer nested loops rather than nested iterators.by maleldil
3/12/2025 at 12:06:52 PM
I agree with the sentiment but I don't think the mocking insult was necessary as per the HN guidelines https://news.ycombinator.com/newsguidelines.htmlby dsego
3/11/2025 at 11:41:27 PM
> I challenge anyone [...] select distinct author from book where pageCount > 1000;
by MathMonkeyMan
3/11/2025 at 11:40:26 PM
Good example actually. You started with a books array, and changed the type to authors half-way through.To know the return type of the chain, I have to read through it and get to the end of each line.
A longBooks array, and map(longBooks, ‘author’) wouldn’t be much longer, but would involve more distinct and meaningful phrases.
I used to love doing chains! I used lodash all the time for things like this. It’s fun to write code this way. But now I see that it’s just a one-liner with line breaks.
by elliottkember
3/13/2025 at 7:11:43 PM
One core reason chaining can be bad is robustness; another longevity/maintenance.Specifically around type-safety, that is knowing that the chained type is what you expect and communicating that expectation to the person who is reading the code without them needing to know the wider context of both the chained-API nor the function the chain resides in. In the context of this article, that means more complexity, and therefore less readability.
I feel this is important because I have worked on many legacy code bases where bugs were found where chains were not behaving as expected, normally after attrition in some other part of the code base, and then you have to become a detective to work out the original intent.
For readability chains are bad, because they can lie about their intent, especially if there’s various semantics that can be swapped. But, like any industry or code base, if their use is consistent, and the api mature/stable, they can be powerful and fast, if.
by frankharrison
3/12/2025 at 3:43:49 AM
FWIW, I'm plenty familiar with functional programming and iterator chains, and I still think for loops often beat them--not only from a "visual noise" perspective, but because complex iterator chains are harder to read than equivalent for loops (particularly when you have to deal with errors-as-values and short circuiting or other patterns) and for simple tasks iterator chains might be marginally simpler but the absolute complexity of the task is so low that a for loop is fine.> But you should be familiar enough that you stop randomly badmouthing map and filter like you have some sort of anti-functional-programming Tourette's syndrome.
I've been moderated for saying much tamer, FYI.
by throwaway894345
3/12/2025 at 8:15:10 AM
Fully agree on the easier to read part. But despite all "coffee is read more often than written" arguments, I see a lot of merit in the functional way. There are a hundred ways to write the loop slightly wrong, or with intended behavior slightly different from the regular. The functional variant: not so much. A small variation from the standard loop in functional style is visible. Very visible. Unintended ones simple won't happen, and the intended ones are an eyesore. An eyesore impossible to miss reading, whereas a subtle variation of the imperative loop is exactly that, subtle. Easy to miss. Readability advantage functional.In my book, keeping simple things simple and the not simple things not simple beats simplicity everywhere. This is actually what I consider a big drawback of functional style: often the not-simple parts are way too condensed, almost indistinguishable from trivialities. But in the loop scenario it's often the reverse.
My happy place, when writing, would be an environment that has enough AST-level understanding to transform between both styles.
(other advantages of functional style: skills and habits transfer to both async and parallel, the imperative loop: not so much)
by usrusr
3/12/2025 at 10:30:29 AM
This is easy to read, but in reality I have found things to typically be a bit less straightforward.Three things typically happens:
1. people who like these chains really like them. And I've seen multiple "one liner expressions" that was composed of several statements anded or ored together needing one or two line breaks.
2. when it breaks (and it does in the real world), debugging is a mess. At least last time I had to deal with it was no good way to put breakpoints in there. Maybe things have changed recently, but typically one had to rewrite it to classic programming and then debug it.
3. It trips up a lot of otherwise good programmers who hasn't seen it before.
by eitland
3/11/2025 at 9:21:52 PM
More readable? How about this:SELECT DISTINCT authors FROM books WHERE page_count > 1000;
by agent327
3/12/2025 at 9:49:56 AM
Yeah but how do you get that data written to a structure database so you get to do that?by namaria
3/12/2025 at 2:38:35 PM
I think, an explicit type would make it even easier to grok: ISet<Author> authorsOfLongBooks =
books
.filter(book => book.pageCount > 1000)
.map(book => book.author)
.distinct()
.toHashset()
Or whatever the equivalent for ISet<Author> is in the respective language. Or IReadonlySet<Author> if the set should be immutable.
by Archelaos
3/12/2025 at 3:56:05 AM
And in a real functional language like F#... let authorsOfLongBooks =
books
|> Seq.filter (fun book -> book.pageCount > 1000)
|> Seq.map (fun book -> book.author)
|> Seq.distinct
...you can set breakpoints anywhere in the pipeline!
by williamcotton
3/11/2025 at 11:10:30 PM
I wouldn’t call 3 long. Which means you’ve picked the softball counterexample. If you were trying to play devil’s advocate, chose a longer legitimate one and show how a loop or other construct would make it better.Three dots is just a random Tuesday.
by hinkley
3/12/2025 at 12:21:36 AM
in the most popular languages this way of programming is hurt by the syntax.this could be something more like
distinct
filter books .pageCount >1000
.author
i think fp looks pretty terrible in js, rust, python, etc
by wegfawefgawefg
3/12/2025 at 1:37:06 PM
When the filter/mapper becomes slightly more involved as it basically always is in real life code, the regular imperative approach is much nicer.by porridgeraisin
3/11/2025 at 4:44:29 PM
authors_of_long_books = set()
for book in books:
if len(book.pages) > 1000:
authors_of_long_books.add(book.author)
return authors_of_long_books
You are told explicitly at the beginning what the type of the result will be, you see that it's a single pass over books and that we're matching based on page count. There are no intermediate results to think about and no function call overhead.When you read it out loud it's also it's natural, clear, and in the right order— "for each book if the book has more than 1000 pages add it to the set."
by Spivak
3/11/2025 at 5:50:31 PM
also it's natural, clear, and in the right orderThat isn't natural to anyone who is not intimately familiar with procedural programming. The language-natural phrasing would be "which of these books have more than thousand pages? Can you give me their authors?" -- which maps much closer to the parent's linq query than to your code.
by tremon
3/11/2025 at 6:01:13 PM
That isn't natural to anyone who is not intimately familiar with procedural programming.This is not about "procedural programming" - this is exactly how this works mentally. For kicks I just asked me 11-year old kid to write down names of all the books behind her desk (20-ish) of them and give me names of authors of books that are 200 pages or more. She "procedurally"
1. took a book
2. flipped to last page to see page count
3. wrote the name of the author if page count was more than 20
The procedural is natural, it is clear and it is in the right order
by bdangubic
3/11/2025 at 6:10:24 PM
That's when you're doing the job, not what the mental representation of the solution. I strongly believe if you ask her to describe the task, she would go:1. (Take the books)->(that have 200 pages or more)->(and mark down the name of the authors)->(only once)
by skydhash
3/11/2025 at 7:18:53 PM
I respectfully disagree. And I think one of the core reason SWEs struggle with functional-style of programming is that it is neither intuitive nor how general-joe-doe’s brain works.by bdangubic
3/11/2025 at 8:29:30 PM
I haven't really encountered software engineers who really struggle with functional style in almost 20 years of seeing it in mainstream languages. It's just another tool that one has to learn.Even the people arguing against functional style are able to understand it.
Strangely, this argument is quite similar to arguments I encounter when someone wants to rid the codebase of all SQL and replace it with ORM calls.
by whstl
3/11/2025 at 10:11:19 PM
Strangely, this argument is quite similar to arguments I encounter when someone wants to rid the codebase of all SQL and replace it with ORM calls.we must be in completely different worlds cause I have yet (close to 30 years now hacking) to see/hear someone trying to introduce ORM on a project which did not start with the ORM to begin with. the opposite though is a constant, “how do we get rid of ORM” :)
I haven't really encountered software engineers who really struggle with functional style in almost 20 years. It's just another tool that one has to learn.
I recall vividly when Java 8 came out (not the greatest example but also perhaps not too bad) having to explain over and over concept of flatMap (wut is that fucking thing?) or even zipping two collections. even to this day I see a whole lot of devs (across several teams I work with) doing “procedural” handling of collections in for loops etc…
by bdangubic
3/11/2025 at 10:28:34 PM
I'm more talking about projects that do start with an ORM, but have judicious (and correct) usage of inline SQL for certain parts. It's not uncommon to see developers spending weeks refactoring into an ORM-mess.The argument is always that "junior developers won't know SQL".
But yeah I've also seen the opposite happening once. People going gung-ho on deleting all ORM code "because there's so much SQL already, why do we need an ORM then".
And then the argument is that "everyone knows SQL, the ORM is niche".
I guess it's a phase that all devs go through in the middle of their careers. They see a hammer and a screwdriver in a toolbox, and feel the need for throwing one away because "who needs more than one tool"...
by whstl
3/11/2025 at 6:05:15 PM
You are describing how to execute the procedure, while the gp is describing what the result should be. Both are valuable, but they're very different.My personal take is that "how to execute" is more useful for lower level and finer grained control, which "what the results should be" is better for wrangling complex logic
by tvier
3/12/2025 at 8:01:03 AM
Your daughter may have implemented it procedurally but your description of the task was functional.by dambi0
3/11/2025 at 5:10:26 PM
fwiw, once Python's introduced there's the third option on the table, comprehensions, which will also be suggested by linters to avoid lambdas: authors_of_long_books: set[Author] = {book.author for book in books if book.page_count > 1000}
These are somewhat contentious as they can get overly complex, but for this case it should be small & clear enough for any Python programmer.
by syklemil
3/11/2025 at 7:13:49 PM
I tried scaling up the original into an intentionally convoluted nonsensical problem to see how a more complicated solution would look like for each approach. Do these look right? And which seems the most readable? # Functional approach
var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory = books
.filter(book =>
book.pageCount > 100 and
book.language == "Chinese" and
book.subject == "History" and
book.author.mentions > 10_000
)
.flatMap(book => book.author.pets)
.filter(pet => pet.is_furry)
.map(pet => pet.favoriteFood)
.distinct()
# Procedural approach
var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory = set()
for book in books:
if len(book.pageCount > 100) and
book.language == "Chinese" and
book.subject == "History" and
book.author.mentions > 10_000:
for pet in book.author.pets:
if pet.is_furry:
favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory.add(pet.favoriteFood)
# Comprehension approach
var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory = {
pet.favoriteFood for pet in
pets for pets in
[book.author.pets for book in
books if len(book.pageCount > 100) and
book.language == "Chinese" and
book.subject == "History" and
book.author.mentions > 10_000]
if pet.is_furry
}
FWIW, for more complex problems, I think the second one is the most readable.
by itsmeknt
3/11/2025 at 7:56:45 PM
I'm more partial to the first one because it keeps a linear flow downwards, and a uniform structure. The second one kind of drifts off, and reshuffling parts of it is going to be … annoying. IME the dot style lends itself much better to restructuring.Depending on language you might also have some `.flat_map` option available to drop the `.reduce`.
by syklemil
3/11/2025 at 8:41:42 PM
True! Good point on the restructuring, I haven't thought about it in that way.I think I like the second approach because the loop behavior seems clearest, which helps me analyze the time complexity or when I want to skim the code quickly.
A syntax like something below would be perfect for me if it existed:
var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory = books[i].author.pets[j].favoriteFood.distinct()
where i = pagecount > 100,
language == "Chinese",
subject == "History",
author.mentions > 10_000
where j = is_furry == True
by itsmeknt
3/13/2025 at 12:46:05 AM
Hm, LINQ query syntax form is kinda going in that direction (from book in books
where book.pagecount > 100
&& book.language == "Chinese"
&& book.subject == "History"
&& book.author.mentions > 10_000
from pet in book.author.pets
where pet.is_furry == true
select pet.favoriteFood)
.Distinct()
But it also demonstrates the...erm, chronic "halfassedness" of LINQ's query syntax form with distinct() not available there and having to fall back to method syntax form anyway...
by dahauns
3/11/2025 at 11:50:15 PM
You would likely approach it in any style with some helper functions once whatever's in the parentheses or ifs starts feeling big. E.g. in the dot style you could fn bookFilter(book: Book) -> bool {
return book.pageCount > 100 and
book.language == "Chinese" and
book.subject == "History" and
book.author.mentions > 10_000
}
var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory = books
.filter(bookFilter)
.flatMap(book => book.author.pets)
.filter(pet => pet.is_furry)
.map(pet => pet.favoriteFood)
.distinct()
by syklemil
3/11/2025 at 8:29:29 PM
Your FP example is needlessly complicated. No one who does FP regularly would write it like that. var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory = books
.filter(book =>
book.pageCount > 100 and
book.language == "Chinese" and
book.subject == "History" and
book.author.mentions > 10_000
)
.flatMap(book => book.author.pets)
.filter(pet => pet.is_furry)
.map(pet => pet.favoriteFood)
.distinct()
Or in Scala: val favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory = (for {
book <- books if
book.pageCount > 100 &&
book.language == "Chinese" &&
book.subject == "History &&
book.author.metnions > 10_000
pet <- book.author.pets if
pet.is_furry
} yield pet.favoriteFood).distinct
Though, most Scala programmers would prefer higher-order functions over for-comprehensions for this.
by tsss
3/11/2025 at 9:48:39 PM
I didn’t see the original, but the FP example here looks fairly idiomatic to me.An alternative, which in FP-friendly languages would have almost identical performance, would be to make the shift in objects more explicit:
var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHistory =
books
.filter(book => isLongChineseBookAboutHistory(book))
.map(book => book.author)
.filter(author => isFamous(author))
.flatMap(author => author.pets)
.filter(pet => pet.isFurry)
.map(pet => pet.favouriteFood)
.distinct()
I slightly prefer this style with such a long pipeline, because to me it’s now built from standard patterns with relatively simple and semantically meaningful descriptions of what fills their holes. Obviously there’s some subjective judgement involved with anything like this; for example, if the concept of an author being famous was a recurring one then I’d probably want it defined in one place like an `isFamous` function, but if this were the only place in the code that needed to make that decision, I might inline the comparison.
by Chris_Newton
3/11/2025 at 8:44:48 PM
Thanks! I have updated my post to use your code. It is indeed much nicer. And yes, I don't write much FP.I just improved the comprehension code as well using the same idea as your code, eliminating an entire list!
by itsmeknt
3/11/2025 at 5:46:40 PM
Without syntax highlighting, "book.author for book in books if book.page_count > 1000" requires a lot more effort to parse because white space like newlines is not being used to separate things out.by davidw
3/11/2025 at 7:14:21 PM
authors_of_long_books: set[Author] = {
book.author
for book in books
if book.page_count > 1000
}
by nicwolff
3/11/2025 at 7:49:26 PM
You've had some answers already, but I also think this is a good argument for syntax highlighting. With tools like tree-sitter it's pretty easy these days to get high quality syntax highlighting, which allows us humans to receive more information in parallel. A lot of the information we pick up in our daily lives is carried through color, and being colorblind is generally seen as a disability (albeit often a mild one which can be undetected for decades).Syntax highlighting in print is more limited because of technological and economic constraints, which might leave just bold, italics and underlines on the table, while dropping color. On screens and especially in our editors where we see the most code, a lack of color is often a self-imposed limitation.
by syklemil
3/11/2025 at 7:53:45 PM
That's not the point though. If you need the syntax highlighting to quickly make out the structure, perhaps the visual layout is not as good as it could be.by davidw
3/11/2025 at 8:00:29 PM
I consider syntax highlighting to be a part of the _visual_ structure. Visibility is more than just whitespace and placement!by syklemil
3/11/2025 at 6:18:06 PM
Set comprehensions are normal in mathematics and, barring very long complex ones, I find them the easiest to parse because they are so natural.They're just a tad more verbose in Python than mathematics because it uses words like 'for' and 'in' instead of symbols.
by xen0
3/11/2025 at 6:55:17 PM
Set comprehension are more idiomatic here (explicit syntax) though filter/map are not that bad too: {*map(_.author, filter(_.page_count > 1000, books))}
It uses lambdas package.
by d0mine
3/12/2025 at 7:43:52 AM
Awesome, giving it a quick scan, authors_of_long_books = set()
Now I know that authors_of_long_books is the empty set. Do I need to bother reading the rest?
by mrkeen
3/11/2025 at 5:59:11 PM
Much of both sides of this argument are opinion, but wrt this comment.> ... no function call overhead.
This code has more function calls. O(n) vs 3 for the original
by tvier
3/11/2025 at 6:23:53 PM
That's not true. The lambdas used in the functional version are each called once for every item in the list.by khaledh
3/11/2025 at 6:45:34 PM
No sane optimizer is going to emit the functional code as a gajillion function calls.by stouset
3/11/2025 at 6:59:27 PM
Yeah, if you treat it as javascript vs python they're likely correct (I'm not that familiar with js). The article and original comment were about function vs imperative though, so I assumed half decent runtimes for both.by tvier
3/11/2025 at 8:36:28 PM
It's not? How could that possibly work when the lambda could throw and it could throw on the nth invocation and your stack trace has to be correct?If I run this in the JS console I get two anonymous stack frames. The first being the console itself.
[1, 2, 3].filter(x => [][0]())
by Spivak
3/11/2025 at 6:59:00 PM
True, but now you're relying on a specific implementation and optimization of the compiler, unless the language semantics explicitly say that lambdas will be inlined.by khaledh
3/11/2025 at 7:03:34 PM
This is true of literally anything and everything your compiler emits. In practice the functional style is much easier to optimize to a far greater degree than the imperative style.by stouset
3/11/2025 at 7:10:29 PM
This is why you shouldn't get into arguments about performance on the internet without highly specified execution environments.I'm going to take my own advice and go back to work :)
by tvier
3/11/2025 at 6:44:52 PM
> You are told explicitly at the beginning what the type of the result will beI would argue that's a downside: you have to pick the appropriate data structure beforehand here, whereas .distinct() picks the data structure for you. If, in the future, someone comes up with a better way of producing a distinct set of things, the functional code gets that for free, but this code is locked into a particular way of doing things. Also, .distinct() tells you explicitly what you want, whereas the intention of set() is not as immediately obvious.
> There are no intermediate results to think about
I could argue that there aren't really intermediate results in my example either, depending on how you think about it. Are there intermediate results in the SQL query "SELECT DISTINCT Author FROM Books WHERE Books.PageCount > 1000"? Because that's very similar to how I mentally model the functional chain.
There are also intermediate results, or at least intermediate state, in your code: at any point in the loop, your set is in an intermediate state. It's not a big deal there either though: I'd argue you don't really think about that state either.
> and no function call overhead
That's entirely a language-specific thing, and volatile: new versions of a language may change how any of this stuff is implemented under the hood. It could be that "for ... in" happens to be a relatively expensive construct in some languages. You're probably right that the imperative code is slightly faster in most languages today, and if it has been shown via performance analysis that this particular code is a bottleneck, it makes sense to sacrifice readability in favor of performance. But it is a sacrifice in readability, and the current debate is over which is more readable in the first place.
> a single pass over books
Another detail that may or may not be true, and probably doesn't matter. The overhead of different forms of loops is just not what's determining the performance of almost any modern application. Also, my example could be a single pass if those methods were implemented in a lazy, "query builder" form instead of an immediately-evaluated form.
In fact, whether this query should be immediately evaluated is not necessarily this function's decision. It's nice to be able to write code that doesn't care about that. My example works the same for a wide variety of things that "books" could be, and the strategy to get the answer can be different depending on what it is. It's possible the result of this code is exactly the SQL I mentioned earlier, rather than an in-memory set. There are lots of benefits to saying what you want, instead of specifying exactly how you want it.
by feoren
3/11/2025 at 10:10:42 PM
Set is a well defined container for unique values. It's much clearer what it is than some non-existent .distinct() function with no definition and unclear return value.Procedural code in JS doesn't say how you want something done any more closely than the functional style variant. for-of is far more generic than .map/.filter() since .map() only works on Array shaped objects, and for-of works on all iterables, even generators, async generators, etc. In any case you're not saying how the iteration will happen with for-of, you're just saying that you want it. Implementation of Set is also the choice of a language runtime. You're just stating what type of container you want.
Sometimes functional style may be more readable, sometimes procedural style may.
by megous
3/11/2025 at 8:49:53 PM
Maybe it's because I'm not familiar with such style, but I don't like how the code hides operational details. That is, if `books` contains one billion books, and the final result should contain about a hundred authors, how much extra memory does this use for intermediate results?by yongjik
3/11/2025 at 9:50:52 PM
The best way to kick the tyres on this kind of question is to plug in something literally infinite. That way if you arrive at an answer you're probably doing something right with regard to space and time usage.For example, use all the prime numbers as an expression in your chain.
import Data.Function
import Data.Numbers.Primes
main = do
let result :: [Int] = primes
& filter (startingWithDigit '5')
& asPairs
& map pairSum
& drop 100000
& take 10
print result
asPairs xs = zip xs (tail xs)
pairSum (a, b) = a + b
startingWithDigit d x = d == head (show x)
> [100960734,100960764,100960792,100960800,100960812]> 3 MiB total memory in use (0 MB lost due to fragmentation)
by mrkeen
3/11/2025 at 8:58:37 PM
This is a valid concern I also reacted a little bit on. One thing to note though is that it is often possible to tell such chains to be lazy and only collect the end result at the end without ever generating any intermediary arrays.Which require the author to actually have an idea how big the numbers are, but that is very often the case regardless of how you write your code.
by yxhuvud
3/12/2025 at 12:18:29 AM
Sometimes I write code like this. Then I delete it and replace it with a for loop, because a loop is just easier to understand.This functional style is what I call write only code, because the only person who can understand it is the one who wrote it. Pandas loves this kind of method chaining, and it's one of the chief reasons pandas code is hard to read.
by patrick451
3/12/2025 at 1:19:03 AM
Chaining calls is an anti-pattern. Not only this is needless duplication of ye olde imperative statements sequence it also makes debugging, modifying ("oh I need to call some function in the middle of the chain, ugh"), and understanding harder for superficial benefit of it looking "cool".It actively hurts maintainability, please stop using it.
by throwA29B