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.
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.
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.
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.
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.
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.
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.
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
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.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.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.
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.
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.
Not necessarily. You can use intermediate variables when necessary.
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()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.
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?
> 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.
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.
var authorsOfLongBooks = books
.filter(book => book.pageCount > 1000)
.map(longBooks => longBooks.author)
.distinct()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.
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.