Live data from Hacker News

What makes code hard to read: Visual patterns of complexity (2023)

seeinglogic.com

211–220 of 383 posts

Re: What makes code hard to read: Visual patterns of complexity (2023)

#211
post #204

Earlier quoted context omitted.

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…

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()…

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!

Re: What makes code hard to read: Visual patterns of complexity (2023)

#212

Earlier quoted context omitted.

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.

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 so…

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.

Re: What makes code hard to read: Visual patterns of complexity (2023)

#213
post #109

> Chaining together map/reduce/filter and other functional programming constructs (lambdas, iterators, comprehensions) may be concise, but long/multiple chains hurt readability This 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 an…

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,…

>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 
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.

Re: What makes code hard to read: Visual patterns of complexity (2023)

#214
post #109

> Chaining together map/reduce/filter and other functional programming constructs (lambdas, iterators, comprehensions) may be concise, but long/multiple chains hurt readability This 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 an…

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?

Re: What makes code hard to read: Visual patterns of complexity (2023)

#215
post #167

Earlier quoted context omitted.

Nope, pure functions are referentially transparent. The key idea is that you can replace the function invocation with a value and it shouldn’t change the program. A regular static function could refer to a file, a database, or it could change some global memory, etc. So, replacing the static function (that causes side-effects) with a pure value wouldn’t result in the same program. Side-effects are usually declarative…

> Nope, pure functions are referentially transparent. The key idea is that you can replace the function invocation with a value and it shouldn’t change the program. [Edit: This is wrong: And idempotent.] Generally you can expect that you can call them as many times as you like and get the exact same result. It _feels_ very safe. > This isn't just a Haskell thing though. I'll write code this way in C# (and have built…

That's not what idempotent means. Idempotent means forall x, f(x)=f(f(x)). Most pure functions are not idempotent. Heck, f(f(x)) doesn't even type-check for most f. The typical name given to always getting the same results is just "pure". It doesn't depend on any implicit state anywhere.

Re: What makes code hard to read: Visual patterns of complexity (2023)

#216
post #168

Earlier quoted context omitted.

Lisp code is an AST. Once that's internalized, the parenthesis fade in the background. Mentally, instead of editing code, you're just arranging the branches. So, when reading, you can usually ignore whole sections as they will evaluate to a single value (side effects are possible, but strongly discouraged)

Having 4 spaces as indentation helps people tease out where the branches even are in languages like C or Python or whatever, rather than the 1 or 2 that you see with a lot of Lisp. And those angled parents make lining things up vertically a teeny bit more difficult.

The one issue with procedural is all the temporary variables and the fact that the variables themselves are intertwined with function calls. With Lisp, the whole branch is self-sufficient. It's a different reading method. Just like reading Prolog requires a different strategy.

Re: What makes code hard to read: Visual patterns of complexity (2023)

#217
post #109

> Chaining together map/reduce/filter and other functional programming constructs (lambdas, iterators, comprehensions) may be concise, but long/multiple chains hurt readability This 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 an…

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?

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.

Re: What makes code hard to read: Visual patterns of complexity (2023)

#218

Earlier quoted context omitted.

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 cha…

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…

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.

Re: What makes code hard to read: Visual patterns of complexity (2023)

#219
post #109

> Chaining together map/reduce/filter and other functional programming constructs (lambdas, iterators, comprehensions) may be concise, but long/multiple chains hurt readability This 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 an…

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,…

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.

Re: What makes code hard to read: Visual patterns of complexity (2023)

#220

Earlier quoted context omitted.

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 cha…

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…

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.
Post reply on HN