Live data from Hacker News

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

seeinglogic.com

371–380 of 383 posts

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

#371
post #368
post #343

Earlier quoted context omitted.

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

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

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.

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

#372

Earlier quoted context omitted.

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

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

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.

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

#373

Earlier quoted context omitted.

Why the for loop instead of mapping the enumerate iterator into Block::new and collect::Vec?

I lack a good answer other than it didn't occur to me, now I have some code to refactor. Thanks!

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.

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

#374

Earlier quoted context omitted.

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

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.

> just... Not write more code

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

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

#375
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…

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.

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

#376

You really should try to pack an unbroken thought as a single line of code as much as possible. That’s the idea behind chaining multiple functions together on one line instead of spreading it out over several lines. Eyes go horizontally more naturally than up and down, it fits our vision’s natural aspect ratio. And stop making deep nestings. Making a single function call per line assigning output to a variable each t…

You wouldn't happen by any chance to be hiring in the US?

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

#377

Earlier quoted context omitted.

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 string s to advanced Cuckoo filter s 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 im…

> Correctness and maintainability trumps performance Great, 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 y…

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

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

#378

Earlier quoted context omitted.

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

> by this metric False 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…

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.

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

#379

Earlier quoted context omitted.

> by this metric False 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…

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…

> I'm not sure reaching for PHP

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

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

#380
post #343
post #316

Earlier quoted context omitted.

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

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

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

Post reply on HN