4 ms·
authors_of_long_books = set() for book in books: if len(book.pages) > 1000: authors_of_long_books.add(book.author) return authors_
by Spivak 2y ago
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."
- syklemil 2y agofwiw, 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.
- davidw 2y agoWithout 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.
- xen0 2y agoSet 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.
- nicwolff 2y agoauthors_of_long_books: set[Author] = { book.author for book in books if book.page_count > 1000 }
- syklemil 2y agoYou'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.
- davidw 2y agoThat'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.
- syklemil 2y agoI consider syntax highlighting to be a part of the _visual_ structure. Visibility is more than just whitespace and placement!
- d0mine 2y agoSet 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.
- itsmeknt 2y agoI 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.
- syklemil 2y agoI'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`.
- deleted 2y ago[deleted]
- itsmeknt 2y agoTrue! 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
- tremon 2y agoalso it's natural, clear, and in the right order That 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.
- bdangubic 2y agoThat 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
- tvier 2y agoYou 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
- skydhash 2y agoThat'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)
- bdangubic 2y agoI 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.
- tvier 2y agoMuch 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
- deleted 2y ago[deleted]
- khaledh 2y agoThat's not true. The lambdas used in the functional version are each called once for every item in the list.
- stouset 2y agoNo sane optimizer is going to emit the functional code as a gajillion function calls.
- khaledh 2y agoTrue, 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.
- stouset 2y agoThis 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.
- tvier 2y agoThis 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 :)
- tvier 2y ago
- feoren 2y ago> You are told explicitly at the beginning what the type of the result will be I 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.
- megous 2y ago
- mrkeen 2y agoAwesome, 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?