NHacker Next
  • new
  • past
  • show
  • ask
  • show
  • jobs
  • submit
What Makes Code Hard to Read: Visual Patterns of Complexity (2023) (seeinglogic.com)
feoren 2 hours ago [-]
> 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 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.
Spivak 1 hours 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."

tremon 14 minutes ago [-]
also 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 3 minutes ago [-]
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

tvier 5 minutes ago [-]
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

boothby 1 minutes ago [-]
Asking because it's not a core language of mine (and you and your parent seem to be in disagreement about this): does modern JavaScript inline those lambdas?
syklemil 54 minutes ago [-]
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.
davidw 18 minutes ago [-]
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.
recursivedoubts 5 hours ago [-]
There is a (large, I believe) aspect of good code that is fundamentally qualitative & almost literary. This annoys a lot of computer programmers (and academics) who are inclined to the mathematical mindset and want quantitative answers instead.

I love dostoyevsky and wodehouse, both wrote very well, but also very differently. While I don't think coding is quite that open a playing field, I have worked on good code bases that feel very different qualitatively. It often takes me a while to "get" the style of a code base, just as a new author make take a while for me to get.

louthy 4 hours ago [-]
I 100% agree with this. One of the best compliments I ever got (regarding programming) was from one of my principal engineers who said something along the lines of "your code reads like a story". He meant he could open a code file I had written, read from top to bottom and follow the 'narrative' in an easy way, because of how I'd ordered functions, but also how I created declarative implementations that would 'talk' to the reader.

I follow the pure functional programming paradigm which I think lends itself to this more narrative style. The functions are self contained in that their dependencies/inputs are the arguments provided or other pure functions, and the outputs are entirely in the return type.

This makes it incredibly easy to walk a reader through the complexity step-by-step (whereas other paradigms might have other complexities, like hidden state, for example). So, ironically, the most mathematically precise programming paradigm is also the best for the more narrative style (IMHO of course!)

zwnow 5 hours ago [-]
I consider code bad if it takes more then 5 seconds to read and understand the high level goal of a function.

Doesn't matter how it looks. If its not possible to understand what a function accomplishes within a reasonable amount of time (without requiring hours upon hours of development experience), it's simply bad.

fasbiner 9 minutes ago [-]
Sounds like you are content to limit yourself to problems that do not contain more irreducible complexity or require more developer context than what fits within five seconds of comprehension.

That's a good rule for straightforward CRUD apps and single-purpose backend systems, but as a universal declaration, "it is simply bad" is an ex cathedra metaphysical claim from someone who has mistaken their home village for the entirety of the universe.

jacobr1 4 hours ago [-]
There is a call-stack depth problem here that is specific to codebases though. For one familiar with the the conventions, key data abstractions (not just data model but convention of how models are structured and relate) and key code abstractions, a well formed function is easy to understand. But someone relatively new to the codebase will need to take a bunch of time switching between levels to know what can be assumed about the state or control flow of the system in the context of when that function/subroutine is running. Better codebases avoid side-effects, but even with good separation there, non-trivial changes require strong reasoning about where to make changes in the system to avoid introducing side-effects and not just passing extra state or around all over the place.

So, I'd take "good architecture" with ok and above readability, over excellent readability but "poor architecture" any day. Where architecture in this context means the broader code structure of the whole project.

zwnow 4 hours ago [-]
But who talked about bad architecture? Good readable code doesn't rule out good architecture. Surely some things are complicated but even then, a dev should be able to quickly see whats going on with minimal expertise in a codebase.
swatcoder 3 hours ago [-]
They're suggesting that readability and "5-second accessibility" are essentially contextual and build on a conceptual language that might be specific to a tool, team, or project.

The novel function that might take "5 seconds to read" for the 20 people contributing to a mature project with a good architecture might nonetheless take 10 minutes for a new hire to decipher because they don't know the local vocabulary (architecture, idioms) and need to trace and interpret things elsewhere in the project and its libraries.

Meanwhile, writing implementations in a way that tried to avoid a local vocabulary altogether might make naive reads easier, but less readable to experienced team member because they're not as concise as they could be.

Your general advice to "make things easily readable" is good advice, but like with writing compelling prose or making accessible graphic design, you need to consider your audience and your choices might look different than the ones somebody else might make.

jacobr1 3 hours ago [-]
The broader problem is "cognitive load to understand the code I'm looking at." There are a variety of factors that lead enabling that. This is a very limited example.

  function isReadyToDoThing(Foo foo) {
    return foo.ready
  }

  function processStuff((Foo foo) {
    if isReadyToDoThing(foo) {
      res = workflowA(foo)
      res2 = workflowB(foo)
      return res && res2
    }
  }

This might be dumb, if isReadyToDoThing is trivial, and it could be easily inlined. Or alternatively it could be a good way to self-document, or annotate a preferred approach (imagine several similar named methods). Regardless if you don't know the code, you'll want to go look at the method, especially if it is in a different file.

But also consider:

  function isReadyToDoThing(Foo foo) {
    return foo.attribute1 && foo.attribute2 && ! otherThing(foo)
  }

This or more complex logic might be encapsulated, in which case this is probably good to separate.

Making these kind of tradeoffs involve thinking about the overall system design, not just the way you structure the code within a given function.

ikrenji 2 hours ago [-]
the purpose of the function should be clear from its name. if its too complex to convey this information it should have a docstring that clearly explains what it does. it's not rocket science
apelapan 1 hours ago [-]
In the general case I fully agree. It is ideal that the name of anything clearly indicates whatever is important about that thing.

But who is the viewer of that name?

How much context can they be assumed to have? The name of the class? The name of of the module? The nature and terminology of the business that the function serves? The nature and terminology of the related subsystems, infrastructure and libraries?

There is a context dependent local optimum for how to name something. There are conflicts of interest and trade-offs have to made.

horsawlarway 2 hours ago [-]
Names are jargon. They are themselves their own form of complexity, and they require a similar timeline to become acquainted with.

Further - the more of them you need (call depth) the worse this problem becomes.

jajko 1 hours ago [-]
Yeah, not disagreeing with what you write but parent is talking about different type of complexity which your description/approach doesn't magically fix, I'd call it spread of complexity
callc 3 hours ago [-]
Generally agree.

Consider reading kernel or driver code. These areas have a huge amount of prerequisite knowledge that - I argue - makes it OK to violate the “understand at a glance” rule of thumb.

sunrunner 4 hours ago [-]
Does this apply to all domains and all 'kinds' of code?

I feel like there's a fundamental difference in the information density between code that, for example, defines some kind of data structure (introducing a new 'shape' of data into an application) versus code that implements a known algorithm that might appear short in line length but carries a lot of information and therefore complexity.

zwnow 3 hours ago [-]
If the algorithm is well known, it's all good as long as the function name for it is somewhat understandable. I have to work with 200 line functions at work and it's a complete, excuse the language, shitshow.
sunrunner 3 hours ago [-]
> as long as the function name for it is somewhat understandable

But does using a function, essentially a box with known inputs and outputs, constitute actually understanding the function? What happens if you need to debug or understand the implementation of it? Now the original name has gone and you're looking at a larger number of differently-named things that hopefully communicate their intent well. But if you need to understand _those_, and so on...

zwnow 3 hours ago [-]
My original comment never was about understanding the implementation details. It was about understanding the high level goal of the function.
jayd16 3 hours ago [-]
So it should take 5 minutes whether it's your language or choice or the assembly it compiles to? Or does it matter how it looks in _that_ case?
andybp85 4 hours ago [-]
for the codebase i work on, i made a rule that "functions do what the name says and nothing else". this way if the function does too much, hopefully you feel dumb typing it and realize you should break it up.
quinnirill 2 hours ago [-]
Then what does the function that calls the split functions get called? foo_and_bar_and_qoo? And if they’re called only under some conditions?
jbeninger 43 minutes ago [-]
I have definitely been guilty of naming functions foo_thenBarSometimes. I wince whenever I write them, but I've never really regretted seeing them later on, even after years. So sometimes it really is a perfectly good name. Sometimes there are two related functions that are often called together and don't have a succinct label for the combined operation.
brulard 1 hours ago [-]
It likely has some higher-level meaning other than just do foo, bar, qoo.

For example if you are calling functions "openDishwasher", "loadDishwasher", "closeDishwasher", "startDishwasher", your function should be called "washDishes". Not always that straightforward, but I believe in 95% it's not difficult to put a name on that. For the rest 5% you need to get creative, or maybe you realize that you didn't group the function calls well enough to have an atomic meaning.

jimmaswell 2 hours ago [-]
This is what xmldoc/jsdoc/etc are for. If it's not 100% obvious from the name, put a summary of the function's assumptions, side effects, output, and possibly an example in the comment-doc. If you do this right, the next programmer will never have to read your source at all (or even navigate to your file! They'll hover over a method call or find it in the dot-autocomplete and see a little tootip with this documentation in it, and know all they need to know). It's an incredible thing when it works. It's a little bit more effort but I don't accept the FUD around "comments become out of date immediately because the code will change" etc. - that should be part of code review.

https://learn.microsoft.com/en-us/dotnet/csharp/language-ref...

intrasight 4 hours ago [-]
> Doesn't matter how it looks.

That's the mindset that the author is trying to counter.

ajross 4 hours ago [-]
> I consider code bad if it takes more then 5 seconds to read and understand the high level goal of a function.

That's something that's possible only for fairly trivial logic, though. Real code needs to be built on an internal "language" reflecting its invariants and data model and that's not something you can see with a microscope.

IMHO obsessive attention to microscope qualities (endless style nitpicking in code review, demands to run clang-format or whatever the tool du jour is on all submissions, style guides that limit function length, etc...) hurts and doesn't help. Good code, as the grandparent points out, is a heuristic quality and not one well-defined by rules like yours.

necrotic_comp 2 hours ago [-]
I agree with this up to a point - having consistent code style with some sort of formatter (gofmt, black, clang-format) goes a long way to reducing complexity of understanding because it unifies visual style.

I suggest that a codebase should read like a newspaper. While there is room for op-eds in the paper, it's not all op-eds, everything else should read as a single voice.

ajross 2 hours ago [-]
> While there is room for op-eds in the paper,

My experience is that projects which value code formatters (and similar rulemaking) tend strongly not to have room for "op-eds", FWIW. And conversely the code bases I've seen with the best/cleanest/most-clearly-expressive code tend strongly to be the ones with fewer rules.

I think the one exception there is in some open source contexts (Linux is the archetype) which receive a fire hose of submissions of questionable quality and maintainership. There, you can use adherence to arbitrary rules as a proxy measurement[1] for attention and effort on the part of the submitter. And that I have no problem with.

But the actual value of code formatters is IMHO extremely low in practice, and the cost isn't "high", but is non-trivial.

[1] The "No Brown M&M's" trick.

zwnow 4 hours ago [-]
I even wrote no matter how it looks?

I meant the goal of your function needs to be grasped within a reasonable amount of time. This works for every codebase.

ajross 4 hours ago [-]
> I meant the goal of your function needs to be grasped within a reasonable amount of time. This works for every codebase.

It really doesn't though. Here's a function of mine. It's maybe 40 lines of logic, so medium-scale. It's part of an intrusive red/black tree implementation for Zephyr. I'm fairly proud of how it turned out, and think that this code is awfully readable given its constraints.

No human being is going to understand fix_extra_red() without having already read and understood the rest of the file, and coming to it with an understanding of the underlying algorithm. Certainly I can't. I can't even get started on maintaining this code that I originally wrote within a five minute time frame, it's an hour at least every time, just to remind myself how it works:

https://github.com/zephyrproject-rtos/zephyr/blob/main/lib/u...

Now maybe this is "bad code", and "good code" could exist for this problem that still meets your requirements. But... if so that's an awfully celestial definition if it's so hard to find.

sunrunner 3 hours ago [-]
This is exactly the kind of example I have in my head for code that constitutes a high level of information density. Adding abstraction and 'literate' constructs to try and make things readable is ultimately deferring the fact that understanding the code here is fundamentally about understanding a specific implementation of an algorithm, and to understand _that_ ultimately needs the reader to build their own clear mental model of both the algorithm itself and how it's been translated into a specific form.

Maybe it's a defeatist attitude, but I feel like sometimes the problem is the problem, and pushing abstractions only works to defer the requirements to understand it. Sometimes you can defer it enough to do useful work, other times you just need to understand the thing.

freetonik 3 hours ago [-]
>This annoys a lot of computer programmers (and academics) who are inclined to the mathematical mindset and want quantitative answers instead.

I find many syntactical patterns that are considered elegant to be the opposite, and not as clear as mathematics, actually. For example, the the ternary operator mentioned in the article `return n % 2 === 0 ?'Even' : 'Odd;` feels very backwards to my human brain. It's better suited for the compiler to process the syntax tree rather than a human. A human mathematician would do something like this:

         ⎧  'Even' n mod 2 = 0
  f(n) = ⎨
         ⎩  'Odd'  n mod 2 ≠ 0
Which is super clear.
pc86 3 hours ago [-]
Well of course if you have the freedom to write a mathematical expression you're going to be able to present it in a way that is clearer than if you have to type monospace characters into a text editor.

I'm not sure it's realistic to expect to be able to type a mathematical expression using ascii more clearly than you can write it by hand (or implement using special unicode characters).

cdirkx 2 hours ago [-]
Quite some years back I worked with JetBrains MPS which used a "projectional editor" instead of a text editor. It was pretty neat to be able to enter "code" as mathematical expressions, or even state machine tables or flow diagrams with actual nodes instead of a text representation.

Sadly not much has happened in that space since then, but it was cool to think about what our tools of the future might look like. (of course ignoring all the practical reasons why we're probably still using regular text files in 100 years)

WillAdams 3 hours ago [-]
Arguably, this is why Literate Programming (see my comment elsethread) didn't take off.
User23 4 hours ago [-]
Mathematicians have recognized the importance of elegance for millennia.
gwbas1c 3 hours ago [-]
This is why code reviews are so critical: They help keep a consistent style while onboarding new team members, and they help a team keep its style (reasonably) consistent.

(Also, see my comment about .editorconfig: https://news.ycombinator.com/item?id=43333011. It helps reduce discussions about style minutia in pull requests.)

mrkeen 4 hours ago [-]
The article's good, but misses my most mentally-fatiguing issue when reading code: mutability.

It is such a gift to be able to "lock in" a variable's meaning exactly once while reading a given method, and to hold it constant while reasoning about the rest of the method.

Your understanding of the method should monotonically increase from 0% to 100%, without needing to mentally "restart" the method because you messed up what the loop body did to an accumulator on a particular iteration.

This is the real reason why GOTOs are harmful: I don't have a hard time moving my mind's instruction-pointer around a method; I have a hard time knowing the state of mutable variables when GOTOs are in play.

klabb3 4 hours ago [-]
> This is the real reason why GOTOs are harmful: I don't have a hard time moving my mind's instruction-pointer around a method; I have a hard time knowing the state of mutable variables when GOTOs are in play.

Well, total complexity is not only about moving the instruction pointer given a known starting point. Look at it from the callee’s pov instead of the call site. If someone can jump to a line, you can’t backtrack and see what happened before, because it could have come from anywhere. Ie you needed global program analysis, instead of local.

If mutability were the true source of goto complexity then if-statements and for loops have the same issue. While I agree mutability and state directly causes complexity, I think goto was in a completely different (and harmful) category.

cle 3 hours ago [-]
Disagree. There's an abstract "information space" that the code is modeling, and you have to move around your mind's instruction pointer in that space. This can be helped or hindered by both mutable and immutable vars--it depends on how cleanly the code itself maps into that space. This can be a problem w/ both mutable and immutable vars. There's a slight tactical advantage to immutable vars b/c you don't have to worry about the value changing or it changing in a way that's misleading, but IME it's small and not worth adopting a "always use immutability" rule-of-thumb. Sometimes mutability makes it way easier to map into that "information space" cleanly.
CharlieDigital 4 hours ago [-]
Maybe it's just me, but TypeScript makes code hard to read.

It's fine if the data model is kept somewhat "atomic" and devs are diligent about actually declaring and documenting types (on my own projects, I'm super diligent about this).

But once types start deriving from types using utility functions and then devs slack and fall back to type inference (because they skip an explicit type), it really starts to unravel because it's very hard to trace fields back to their origin in a _deep_ stack (like 4-5 levels of type indirection; some inferred, some explicit, some derived, some fields get aliased...).

    type Dog = {
      breed: string
      size: "lg" | "md" | "sm"
      // ...
    }

    type DogBreedAndSize = Pick<Dog, "breed" | "size">

    function checkDogs(dogs: Dog[]) : DogBreedAndSize[] {
      return dogs.map(d => /* ... */)
    }

    const checkedDoggos = checkDogs([])
Versus:

    function checkDogs(dogs: Dog[]) {
      // ...
    }
Very subtle, but for large data models with deep call stacks, the latter is completely unusable and absolutely maddening.
bluefirebrand 4 hours ago [-]
I agree that functions should probably specify their output type, MOSTLY to enforce that all paths that return from that function must adhere to that type

I've seen plenty of regressions where someone added a new condition to a function and then returned a slightly different type than other branches did, and it broke things

However, I don't think there is much value in putting types on variable declarations

In your example,

`const checkedDoggos = checkDogs([])` is good. Just let checkedDoggos inherit the type from the function

I have a codebase I'm working on where the linter enforces

`const checkedDoggos: DogBreedAndSize[] = checkDogs([])`

It is very silly and doesn't add much value imo

CharlieDigital 3 hours ago [-]
I want it on the other side (on the function return) so that it's consistently displayed in type hints and intellisense so I don't have to navigate the code backwards 3-4 layers to find the root type (do you see what I'm saying?)

    function checkDogs(dogs: Dog[]) : DogBreedAndSize[] {
      return dogs.map(d => /* ... */)
    }
^^^ That's where it's important to not skip the type def because then I can see the root type in the editor hints and I don't need to dig into the call stack (I know the end result is the same whether it's on the assignment side or the declaration side, but it feels like ensuring it's always on the declaration side is where the value is)
dkdbejwi383 2 hours ago [-]
I'd prefer to have some type information over nothing if the choice were between TypeScript with some inferred return types, versus JavaScript where you're never really sure and constantly have to walk back up/down the stack and keep it in your mind.
CharlieDigital 1 hours ago [-]
I'd say on backend, my preference is statically something like C#. Statically typed but enough type flexibility to be interesting (tuples, anonymous types, inferred types, etc)
stared 3 hours ago [-]
My pet peeve:

    function getOddness4(n: number):
      if (n % 2 === 0):
        return "Even";
      return "Odd";
While it is shorter, I prefer vastly prefer this one:

    function getOddness2(n: number):
      if (n % 2 === 0):
        return "Even";
      else:
        return "Odd";
Reason: getOddness4 gives some sense of asymmetry, whereas "Even" and "Odd" are symmetric choices. getOddness2 is in that respect straightforward.
bogomog 10 minutes ago [-]

  function getOddness(n: number)
    return (n % 2 === 0)
      ? "Even"
      : "Odd";
Lowest boilerplate makes it the most readable. If working in a language with the ternary operator it ought to be easily recognized!
brulard 1 hours ago [-]
Nice example of how subjective this is. I immediately thought the first one without "else" is clearly the winner.

This is the problem with formatting rules. A codebase needs to have consistent style, even though that might mean nobody is fully happy with it.

I for example can not stand semicolons in JavaScript. It is just a visual clutter that is completely redundant, and yet some people really want it there.

__oh_es 1 hours ago [-]
I feel guard clauses/early returns end up shifting developer focus on narrowing the function operation, and not the happy path with some after thought about other conditions it could handle.

IME else’s also end up promoting further nesting and evaluating edge cases or variables beyond the immediate scope of happy path (carrying all that context!).

CatAtHeart 3 hours ago [-]
I personally prefer the former as you can visually see the return one level of indentation below function name. It shows a guaranteed result barring no early-exits. Something about having the return embedded lower just seems off to me.
cess11 2 hours ago [-]
I would add a blank line to push 'return "Odd";' from the if, and also add brackets around the if-body if the language allows.

There are situations where I allow else, they tend to have side effects, but usually I refactor until I get rid of it because it'll come out clearer than it was. Commonly something rather convoluted turns into a sequence of guards where execution can bail ordered based on importance or execution cost. It isolates the actual function/method logic from the exit conditions.

davidw 16 minutes ago [-]
This is interesting. Something I long wondered about Lisp code, was how having glyphs that are angled (parenthesis) without much indentation in many cases might be difficult to read just because of the visual aspects of it.

     (((
       ((
         (
Takes some staring at to figure out what's what.
James_K 5 hours ago [-]
I my view, code complexity is best expressed in the size of it's syntax tree, with maybe an additional term for the number of unique nodes. The real mistake here is the assumption that local reductions in complexity make a meaningful difference to overall complexity. Small local decreases in complexity may guide you towards the local minimum of complexity, but will never substantially change the complexity of the code-base overall. All measurements of code complexity are essentially as good as asking "how much code do you have".
bluGill 5 hours ago [-]
That is ultimately by problem with the article. It isn't a bad investigation but it cannot stand alone. I never review on function in isolation. It always needs to be in context of what calls it (and often what it calls).
zesterer 1 hours ago [-]
I've never understood the hate for variable shadowing. Maybe it's because I mostly use Rust, but I've always found it a useful boon for readability. You often want to extract/parse/wrap/package some value within the middle of a function in a manner that changes its type/form but not its semantic purpose. Shadowing the old value's variable name is brilliant: it communicates that there's a step-change in the responsibilities of the function, demarcating layers from one-another and preventing accidental use of the old value.
syklemil 42 minutes ago [-]
> I've never understood the hate for variable shadowing. Maybe it's because I mostly use Rust,

That's likely a good chunk of it. My impression is it's more acceptable in languages where you have a very correctness-focused compiler, and `rustc` is that both with types and liveness/ownership. In a language where it's less clear when you copy values or hand out mutable references, or where implicit conversions occur on type mismatches, it's gonna be a different experience.

I think this article is best read as js/ts-specific advice, e.g. the split between null and undefined also isn't something you have to worry about in most other languages, and the semantics of various `?` and `?.` operators can vary a lot.

gwbas1c 3 hours ago [-]
> For long function chains or callbacks that stack up, breaking up the chain into smaller groups and using a well-named variable

> Is the second one marginally less efficient?

> Yes.

No, both versions are just as efficient:

In both versions, the same objects are allocated, stored on the heap, and garbage collected. The difference in efficiency comes down to the compiler.

For the second version, the compiler should observe that each variable is only used immediately after it's declared, and thus treat those objects as "out-of-scope" as if it's a chained function call.

bluGill 5 hours ago [-]
Towards the end he had an example of splitting a sequence of "graph.nodes(`node[name = ${name}]`).connected().nodes().not('.hidden').data('name');" adding variable between some of the . in there and claimed it was marginally less efficient. This is sometimes true, but if it ever is you need to talk to your tool vendors about a better optimizes. If you are working in a language without an optimizer than the marginal difference from that optimization applied by hand will be far smaller than the performance improvements you will get by rewriting in a language with an optimizer. Either way, readability trumps performance: either because the performance is the same, or if performance mattered you would have choosen a different language in the first place.
evnp 4 hours ago [-]
I found this example troubling because once all the line noise is added,

first-op second-op third-op

fourth-op fifth-op sixth-op

feels so much more impenetrable than

- first-op

- second-op

- third-op

- fourth-op

- fifth-op

- sixth-op

The point of functional styles isn't purely brevity (as implied by the commentary around this example), it also puts a focus on the clear sequence of operations and helps reduce "operators and operands" beneficially as discussed early in the post. In general I found the post oddly dismissive of these styles, instead of weighing tradeoffs as I would hope.

syklemil 29 minutes ago [-]
IME what we want is generally for the code to be close to the left margin and flow predictably downwards. The example with intermediate values has a lot more value to me for complex instantiations, where we can avoid nesting like it's json or yaml by using some helper variables. That problem is fundamentally the same as with deeply nested if/while/try/etc: It gets hard to visually tell what's in which scope. (Rainbow indent guides help, but they're still mitigation for a situation that can be eliminated.)

But completely linear dot chains? They're fine.

isleyaardvark 4 hours ago [-]
I’d add that I follow that approach because the optimizers are more likely to optimize readable code than weird hacks.
jt2190 5 hours ago [-]
Kudos to seeinglogic for trying to quantify that “readablity” is. We need a lot more of this. (I feel like the most common definition of readability in use today is “readable to me“.)

I have a half-baked thought that we could find the actual dimensions of readability if we gave a test to a very large group of people and asked them to pick a sentence that describes what the code does. Each question would be timed. The questions that most people answered correctly in the shortest average time would provide us with examples of “real-world readable” code, and more importantly, might help us identify some truly not-readable practices.

I predict we’d see respondents start to cluster around various things, like “how long have they been programming?“, “do they understand programming paradigm X?“, etc. Perhaps the results would shift over time, as various things came into and out of fashion.

James_K 5 hours ago [-]
I actually don't see any value in it. Code readability is similar to language readability in that it is mostly a concern for people who don't know a language and can be addressed by spending time with it. The real issue of programming is code complexity which you cannot determine from metrics about individual pieces of code. The problem exists in the relationships between functions rather than the implementation decisions in the bodies of those functions.
alpinisme 5 hours ago [-]
Yes one of the core challenges here is that we learn to read code. So what you learn to read and write shapes what you find readable. And lots of factors shape what you learn to read and write, including what you are trying to do, who you’re doing it with, what besides coding you knew how to do ahead of time, what other languages you know, etc. One stark possibility is that a lot of “readability” concerns after the low hanging fruit is gone (like don’t name variables with arbitrary irrelevant or misleading names) are really just about consensus building: maybe there are no right answers that transcend the particular group of programmers you are trying to work with.
bluGill 5 hours ago [-]
As an example, for my first decade of programming I worked on code where the coding style banned the ?: operator, so I didn't use it and found such code hard to read the few times I encountered it. Then I got a new job where one of the programmers really liked that operator and so I was forced to learn how to read it, now such code is more readable then the if statements to me - when used in the way we use it on this project.
usrbinenv 5 hours ago [-]
That's very one-dimensional. It's usually easy to tell what the code does, but what's hard is to modify or add functionality to it. And this is because of various levels of abstractions that hide how things are interconnected.
usrbinenv 5 hours ago [-]
Another thing that comes to mind is the level at which one is familiar with a particular style of code organization & a set of abstractions. For example, Rails devs really have absolutely no problem getting up to speed with any given Rails app, but people who don't practice Ruby/Rails as their primary language/framework often complain how complicated and magical it is.
esafak 5 hours ago [-]
Poor abstractions. Good abstractions make it easier to change things, by decomposing the code into cohesive pieces with low coupling so that you can swap them out and having to think about the surrounding pieces beyond their interfaces. A good interface is small and logical.
bluGill 5 hours ago [-]
Good abstractions make it easy to change the things that will change. Abstractions always make some changes harder and some easier, but good ones make hard the things you wouldn't change anyway.
deepsun 1 hours ago [-]
> there’s a better chance that the programmer forgets to properly handle all of the possibilities

Author totally forgot about IDEs. Yes, I know some coders frown upon IDEs and even color coding (like Rob Pike), but any modern code editor will shout loudly about an unhandled null-pointer check.

Also depends on the language, e.g. it's less reliable in Javascript and Python, but in static typed languages it's pretty obvious at no additional cognitive load.

stopthe 2 hours ago [-]
I always shrugged off the concept of code metrics (from LoCs to coverage) as a distraction from getting actual things done. But since doing more code-review I started to lack a framework to properly explain why a particular piece of code smells. I sympathize with the way the author cautiously approaches any quantitative metrics and talks of them more like heuristics. I agree that both Halstead Complexity and Cognitive Complexity are useless as absolute values. But they can be brought up in a conversation about a potential refactoring for readability.

What I didn't find is a mention of a context when reading a particular function. For example, while programming in Scala I was burnt more than once by one particular anti-pattern.

Suppose you have a collection of items which have some numerical property and you want a simple sum of that numbers. Think of shopping cart items with VAT tax on them, or portfolio positions each with a PnL number. Scala with monads and type inference makes it easy and subjectively elegant to write e.g.

  val totalVAT = items.map(_.vat).sum
But if `items` were a `Set[]` and some of the items happened to have the same tax on them, you would get a Set of numbers and a wrong sum in the end.

You could append to the list of such things until the OutOfMemoryError. But it's such a beautiful and powerful language. Sigh.

slevis 3 hours ago [-]
Do people really really agree with "Shorthand constructs that combine statements decreases difficulty"? The author even identifies a problem with the example from the original guide.
Jensson 2 hours ago [-]
Everyone agrees that well made shorthand constructs decreases difficulty, since every programmer uses those every day. Things like function calls, while loops etc are all shorthands for different kinds of jump statements combined with register manipulation. Even assembly uses some of those, and I don't think anyone seriously codes in machine code.
fauigerzigerk 2 hours ago [-]
>The author even identifies a problem with the example from the original guide

He does, but I'm not sure he's right. The code snippet appears to be in C# or Dart and neither has undefined.

marcosdumay 3 hours ago [-]
No, almost everybody disagree with it as a general statement.

Some people disagree to a point where they want languages to have only a handful different constructs. But most people will disagree at some amount of language complexity.

BorgHunter 5 hours ago [-]
I think things like Halstead complexity or cyclomatic complexity are more heuristic than law. To read code, the most important thing to me is the abstractions that are built, and how effectively they bury irrelevant complexities and convey important concepts.

As an example, I recently refactored some Java code that was calling a service that returned a list of Things, but it was paged: You might have to make multiple calls to the service to get all the Things back. The original code used a while loop to build a list, and later in the same function did some business logic on the Things. My refactoring actually made things more complex: I created a class called a Spliterator that iterated through each page, and when it was exhausted, called the service again to get the next one. The upside was, this allowed me to simply put the Things in a Stream<Thing> and, crucially, buried the paged nature of the request one level deeper. My reasoning is that separating an implementation detail (the request being paged) from the business logic makes the code easier to read, even if static code analysis would rate the code as slightly more complex. Also, the code that handles the pages is fairly robust and probably doesn't need to be the focus of developer attention very often, if ever, while the code that handles the business logic is much more likely to need changes from time to time.

As programmers, we have to deal with a very long chain of abstractions, from CPU instructions to network calls to high-falutin' language concepts all the way up to whatever business logic we're trying to implement. Along the way, we build our own abstractions. We have to take care that the abstractions we build benefit our future selves. Complexity measures can help measure this, but we have to remember that these measures are subordinate to the actual goal of code, which is communicating a complex series of rules and instructions to two very different audiences: The compiler/interpreter/VM/whatever, and our fellow programmers (often, our future selves who have forgotten half of the details about this code). We have to build high-quality abstractions to meet the needs of those two audiences, but static code analysis is only part of the puzzle.

WillAdams 3 hours ago [-]
dchristian 3 hours ago [-]
This talks about the "what" of the code, but you have to also convey the "why".

If you have a well understood problem space and a team that is up to speed on it, then the "why" is well established and the code is the "what".

However, there are often cases where the code is capturing a new area that isn't fully understood. You need to interleave an education of the "why" of the code.

I was once asked to clean up for release 10k lines of someone else's robotics kinematics library code. There weren't any comments, readmes, reference programs, or tests. It was just impenetrable, with no starting point, no way to test correctness, and no definition of terms. I talked to the programmer and he was completely proud of what he had done. The variable names were supposed to tell the story. To me it was a 10k piece puzzle with no picture! I punted that project and never worked with that programmer again.

gwbas1c 3 hours ago [-]
> To bring closure to the story at the beginning of this post, the codebase that was breaking my brain had several anti-patterns, specifically related to items 1, 2, 3, 6, and 8 in the list above.

FYI: If you get into this situation in C#, the .editorconfig file and the "dotnet format" command are a godsend.

I inherited a very large, and complicated C# codebase with a lot of "novice" code and inconsistent style. I spent about 3 weeks adding rules to .editorconfig, running "dotnet format" and then manually cleaning up what it couldn't clean up. Finally, I added a Github Action to enforce "dotnet format" in all pull requests.

As a result: 1: The code was significantly more readable. 2: It trapped mistakes from everyone, including myself.

There are a few areas where we have to disable a rule via #pragma; but these are the exception, not the norm.

begueradj 1 hours ago [-]
The notion of "readable code" involves 2 parties:

a) The skill level of the person who produces the code

b) The skill level of the person who reads the code.

Most of the time, we tend to blame the a) person.

Izkata 3 hours ago [-]
I think the only one I disagree with here is the function chains example. I may agree with a different example, but with this one I find the chained version without variables much easier to understand because I'm traversing the graph visually in my head, while the variables are additional state I have to keep track of in my head.

----

Really I was hoping this would be about actual visual patterns and not syntax. It's my major issue with how strict code linting/autoformatting is nowadays.

For example, the "black" formatter for python requires this:

    drawer.ellipse((10, 10, 30, 30), fill=(256, 256, 0))
    drawer.ellipse((370, 10, 390, 30), fill=(256, 256, 0))
    drawer.arc((20, 0, 380, 180), 15, 165, fill=(256, 256, 0), width=5)
The first argument is (x1, y1, x2, y2) of a bounding box and black wants to align x1 for "ellipse" with y1 of "arc". Do people really find that more readable than this?

    drawer.ellipse( (10,  10,  30,  30), fill=(256, 256, 0) )
    drawer.ellipse( (370, 10, 390,  30), fill=(256, 256, 0) )
    drawer.arc(     (20,   0, 380, 180), 15, 165, fill=(256, 256, 0), width=5 )
Or perhaps something more common in python, kwargs in function arguments. No spacing at all is standard python style:

    Foobar.objects.create(
        name=form.name,
        owner=user,
        location=form.location,
        source=form.source,
        created=time.now,
    )
Instead for me it's much easier to read this, since I don't have to scan for the "=" and mentally parse each line, it's just a table:

    Foobar.objects.create(
        name        = form.name,
        owner       = user,
        location    = form.location,
        source      = form.source,
        created     = time.now,
    )
But no linters/formatters accept such spacing by default. I think flake8 (linter) could be configured to ignore whitespace like this, but black (formatter) can't.
12_throw_away 14 minutes ago [-]
Vertical alignments really makes me want to create a devtool stack that operates at more of an AST level and less of an "monospaced text + extra bells and whistles" level.

Aligning similar expressions for ease of reading seems like exactly the sort of thing an editor should display for us without requiring some arbitrary number of spaces to be stored in a text file ...

jraph 3 hours ago [-]
There are two issues with such alignment that may make the "less readable" version a bit better despite being, indeed, arguably less readable:

- a modification might lead you to realign several lines, making diffs noisier (though you can ignore white-spaces when displaying diffs, but the commits still hold these lines making navigating in the history less straightforward

- we all have our own preferences wrt alignment, and your way of aligning or what you decide to align might be different from someone else, and this can lead to friction

Worse is probably better here, as much as I like aligned stuff, I think black is right in this case :-)

moi2388 2 hours ago [-]
I find the main problem is not wanting to split up functions.

I greatly prefer small helper functions, so that a more complicated one becomes more readable.

Even declaring a little local variable which explains in English what the condition you’re going to test is supposed to do is greatly appreciated

WillAdams 5 hours ago [-]
Why not apply a programming methodology which allows one to leverage a rich set of tools which were created for making text more readable and visually pleasing?

http://literateprogramming.com/

For a book-length discussion of this see:

https://www.goodreads.com/book/show/39996759-a-philosophy-of...

previously discussed here at: https://news.ycombinator.com/item?id=27686818 (and other times in a more passing mention --- "Clean Code" adherents should see: https://news.ycombinator.com/item?id=43166362 )

That said, I would be _very_ interested in an editor/display tool which would show the "liveness" (lifespan?) of a variable.

Chris_Newton 4 hours ago [-]
I find Literate Programming interesting partly because it’s almost the opposite of the much-advocated “many small functions” style. You could literally be writing a book that explains your program, and the code becomes almost secondary material to illustrate the main text rather than the main asset itself.

I did once write a moderately substantial application as a literate Haskell program. I found that the pros and cons of the style were quite different to a lot of more popular/conventional coding styles.

More recently, I see an interesting parallel between a literate program written by a developer and the output log of a developer working with one of the code generator AIs. In both cases, the style can work well to convey what the code is doing and why, like good code comments but scaled up to longer explanations of longer parts of the code.

In both cases, I also question how well the approach would continue to scale up to much larger code bases with more developers concurrently working on them. The problems you need to solve writing “good code” at the scale of hundreds or maybe a few thousand lines are often different to the problems you need to solve coordinating multiple development teams working on applications built from many thousands or millions of lines of code. Good solutions to the first set of problems are necessary at any scale but probably insufficient to solve the second set of problems at larger scales on their own.

WillAdams 4 hours ago [-]
The Axiom computer algebra folks seem to manage well --- I'm pretty sure that's the largest publicly available literate program which is available for inspection.

I've been working to maintain a list of Literate Programs which have been published (as well as books about the process):

https://www.goodreads.com/review/list/21394355-william-adams...

I'd be glad of any I missed, or other links to literate programs.

The list of projects so tagged on Github may be of interest:

https://github.com/topics/literate-programming

mbarbar_ 2 hours ago [-]
Another not on the list is Scheme 9 from Empty Space. I can't speak to its quality though as I've never looked at the resulting book, just perused the stripped source a little a while back.

https://www.t3x.org/s9fes/

WillAdams 2 hours ago [-]
Thanks! I've added that to the list.

I'd be grateful of any other such texts.

donatj 4 hours ago [-]
Your link for literate programming just gets me a Parallels H-Sphere error
WillAdams 4 hours ago [-]
Try searching for "literate programming" --- it should be the top link.
Pannoniae 6 hours ago [-]
This is probably a bit of a cheap dismissal.... but I think this article misses the forest for the trees. They set a standard which they manage to meet - but do not really introspect on whether the standard is appropriate or not.

All of these metrics (except variable liveness) are on a method/function level. Guess what this encourages? Splitting everything into three-line methods which makes the codebase a massive pile of lasagna full of global or shared state.

If a method is long to read from top to bottom, the answer isn't always splitting it into 5 smaller ones, sometimes life just has inherent complexity.

LandR 6 hours ago [-]
> If a method is long to read from top to bottom, the answer isn't always splitting it into 5 smaller ones, sometimes life just has inherent complexity.

Yes! This.

I find it much easier to parse a long function where I can scroll down it and just read it top to bottom, then having a function which calls out to lots of other functions and I'm jumping around the code base, back and forward.

Just reading the long function top to bottom, where I can very easily just scroll up a bit is so much easier to keep in my head.

Even worse is when you go to definition on the method and you get 5 options, and you have to figure out which one would actually get called given the current path through.

mpweiher 5 hours ago [-]
I think the issue is whether the functions that are split out are actually useful abstractions.

If they are, you should not have to jump around the code-base, you should be able to just read the invocation and know what it does, without leaving the source function.

As an example, you probably don't whip out your kernel source code when you encounter a call to write(). At least not usually. You just know what it does and can keep going.

You probably also don't look at the generated assembly code, and maybe look up the instruction reference for your favorite microprocessor when you encounter an arithmetic operator. You just assume that you know what it does, even if that may not be 100% correct in every case.

Those are good, useful abstractions.

That's what we need to strive for when we crate code.

dijksterhuis 5 hours ago [-]
> [than] having a function which calls out to lots of other functions and I'm jumping around the code base, back and forward.

i agree with longer functions and less jumping around, but there's also some nuance i find. I sometimes find converting a complicated multi-line condition into something like the below is much easier for me to read, so long as the function is named in a clear way and the function definition is just above the big function it gets called by (never in a different file!)

    def is_something_valid(something, foo, bar):
        return something > 0 and something != foo and something > bar

    if is_something_valid(something, foo, bar):
it's like a convenience reading shortcut so i don't have to parse the logic in my head if i don't want to. also makes the condition nice and easy to test.

then again, can also do it the grug-brained way

    gt_zero = something > 0 
    ne_foo something != foo
    gt_bar something > bar

    if gt_zero and ne_foo and gt_bar:
jabiko 5 hours ago [-]
I think you are making a good point but if this function is only used in one place I would personally prefer to just use a variable:

    something_is_valid = something > 0 and something != foo and something > bar

    if something_is_valid:
        # do stuff
That way you can document the intention of the condition using the variable name while at the same time keeping the locality of the check.
jodrellblank 3 hours ago [-]
> can also do it the grug-brained way

This way reads like:

    x = 1 // set variable x equal to 1
in that gt_zero echoes what the > operator does and says nothing about intent. Comparing, e.g.

    gt_zero = space > 0     // there is some space I guess?

    space_for_logfile = space > 0   // oh, logfiles need space > 20 there's the mistake.
robert_dipaolo 5 hours ago [-]
I mostly agree, but for short one liners and where there will be no reuse elsewhere, instead of a function I prefer;

  something_is_valid = something > 0 and something != foo and something > bar
  if something_is_valid:
    # ....
It achieves the same thing without needing to scroll.
_dark_matter_ 5 hours ago [-]
100%. Worst is when the called function is in a separate file, and the most upsetting is when it's the _only_ function in the file. I really wish IDEs or tools like Sourcegraph could handle this better.
rudasn 5 hours ago [-]
What are talking about?! That's my favourite part when reading code! :P
jghn 3 hours ago [-]
For longer functions vs bouncing between smaller functions my experience has been that this is one of those things where people are one way or the other. And they almost never change their preference. If your coworkers are all the same as you, that's great. If they're not, prepare for battle.
michaelcampbell 5 hours ago [-]
> the answer isn't always splitting it into 5 smaller ones

To someone who just read a book about it, it is. I've heard this called "rabbit hole" programming; it's function after function after function, with no apparent reason for them other than the line count. It's maddening.

LandR 5 hours ago [-]
I think the whole Uncle Bob clean code movement has a lot to answer for.
bluGill 5 hours ago [-]
While then Uncle Bob clean code movement has a lot of answer for, it is far far better than much of what came before. I've had to work with 60,000 line functions where #ifdefs worked across brace boundaries

    ...
    #ifndef foo
    break;
    case SomeCondition:
       doSomething().
    #endif
       moreCode();
       break;
I'll take the worse uncle Bob can throw at me over that mess.
cardanome 5 hours ago [-]
While I think that there is no harm in longer functions as long as the code is linear, I think the the problem is that people abstract the wrong way.

The main issue I see is people writing functions that call functions that call functions. No. Don't. Have one main function that is in control and that calls all the sub-functions. Sub-functions do not call functions on their same level, ever. Yes, those sub-function could have their own helper function and so on but the flow always needs to be clearly one-directional. Don't go across and create a spaghetti mess. Trees not graphs.

Keep your code structure as flat as possible.

> massive pile of lasagna full of global or shared state.

Yeah, the skill is to avoid shared state as much as possible. Handling state centrally and opting for pure functions as much as possible.

hobs 5 hours ago [-]
I would use the term irreducible complexity - you can move it around but you cant get rid of it, and spreading it all over your code smoothly and evenly makes it 10x harder to change or reason about it.
5 hours ago [-]
mannykannot 5 hours ago [-]
I would like to add something to the point made here:

"For long function chains or callbacks that stack up, breaking up the chain into smaller groups and using a well-named variable or helper function can go a long way in reducing the cognitive load for readers. [my emphasis]

  // which is easier and faster to read?
  function funcA(graph) {
    return graph.nodes(`node[name = ${name}]`)
      .connected()
      .nodes()
      .not('.hidden')
      .data('name');
  }

  // or:
  function funcB(graph) {
    const targetNode = graph.nodes(`node[name = ${name}]`)
    const neighborNodes = targetNode.connected().nodes();
    const visibleNames = neighborNodes.not('.hidden').data('name')

    return visibleNames;
  }
The names of the functions being called are rather generic, which is appropriate and unavoidable, given that the functions they compute are themselves rather generic. By assigning their returned values to consts, we are giving ourselves the opportunity to label these computations with a hint to their specific purpose in this particular code.

In general, I'm not a fan of the notion that code can always be self-documenting, but here is a case where one version is capable of being more self-documenting than the other.

crazygringo 4 hours ago [-]
I'm almost always a fan of more rather than less commenting and self-documentation...

...but in this example I find the first to be far faster and easier to read. The "labeled" versions don't add any information that isn't obvious from the function names in the first.

If you were giving business logic names rather than generic names (e.g. "msgRecipient", "recipientFriends", "visibleFriends" then I could see more value. But even then, I would find the following the easiest:

    function funcA(graph) {
        return (graph
          .nodes(`node[name = ${name}]`)  // message recipient
          .connected()  // recipient friends
          .nodes()
          .not('.hidden')  // inside current scroll area
          .data('name')
          );
      }
This keeps the code itself simple and obvious, prevents a ton of repetition of variable names, and allows for longer descriptions than you'd want in a variable name.
ledauphin 3 hours ago [-]
_thank you_. this is the comment i came here desperately hoping somebody had already made.

It's not that names are bad - it's that when you use intermediate variables, my brain has to check whether any of the variables are used more than once - i.e., is the flow here completely linear, or is there a hidden branching structure?

the chain of methods approach makes it _completely_ clear that there is no 'tree' in the code.

If you want names (and that's a fine thing to want!) then _either_ comments or defining separate _functions_, e.g `function messageRecipient`, `function friends`, `function visibleToScroll`) is the way to go. Although with many languages that don't have a built-in pipe operator, it becomes harder to express the nice linear flow in a top-to-bottom arrangement if you take the function route. A good reason for languages to keep converging toward syntax for pipes!

For my money, you only define those functions if you want to reuse them later - additional indirection is not usually helpful - so comments would be my choice if there were no other uses of these selectors.

mannykannot 3 hours ago [-]
I agree that commenting appropriately is desirable (I do more that I used to.) I also like the idea of const being the default, and for syntax highlighting that clearly distinguishes mutables.
mannykannot 4 hours ago [-]
You are right, using comments is even more effective in going from the generic to the specific - but then, there's a vocal minority who insist that comments are not only unnecessary, but a clear indication that you are doing it wrong. I must admit that I doubt many of them would endorse the use of auxiliary consts in the manner of the original example, either.
fs_software 4 hours ago [-]
This came up at work the other day re: client-side code readability.

In one camp, folks were in favor of component extraction/isolation with self-documenting tests wherever possible.

In the other camp, folks argued that drilling into a multi-file component tree makes the system harder to understand. They favor "locality of behavior".

Our consensus - there needs to be a balance.

rob74 4 hours ago [-]
Those were exactly my thoughts while reading this article: if your codebase (over)uses inheritance, interfaces, traits, facades, dependency injection etc. etc. to thinly spread any given functionality over several files, no amount of formatting or nice naming is going to save you...
snitzr 4 hours ago [-]
Shoutout to the pipe operator in R. The code equivalent of "and then." It helps to unnest functions and put each action on one line. I know R is more for stats and data, but I just think it's neat.
stared 4 hours ago [-]
Yes, dplyr pipes are wonderful.

Also, for the same reason, I find JavaScript list comprehensions cleaner than those in Python - as in the former it is possible to chain maps and filters.

Also, now there is a new pipe syntax in SQL, that adds a lot to readability.

memhole 3 hours ago [-]
For anyone interested in this as design, it’s called method chaining.
closed 2 hours ago [-]
I think piping and method chaining are a little bit different.

Piping generally chains functions, by passing the result of one call into the next (eg result is first argument to the next).

Method chaining, like in Python, can't do this via syntax. Methods live on an object. Pipes work on any function, not just an object's methods (which can only chain to other object methods, not any function whose eg first argument can take that object).

For example, if you access Polars.DataFrame.style it returns a great_tables.GT object. But in a piping world, we wouldn't have had to add a style property that just calls GT() on the data. With a pipe, people would just be able to pipe their DataFrame to GT().

memhole 1 hours ago [-]
Good to know. I assumed it was all done via objects or things like objects.

So is piping more functional programming?

closed 40 minutes ago [-]
I think it's often a syntax convenience. For example, Polars and Pandas both have DataFrame.pipe(...) methods, that create the same effect. But it's a bit cumbersome to write.

Here's a comparison:

* Method chaining: `df.pipe(f1, a=1, b=2).pipe(f2, c=1)`

* Pipe syntax: `df |> f1(a=1, b=2) |> f2(c=1)`

memhole 37 minutes ago [-]
Ok, that’s helpful. Thanks!
morning-coffee 3 hours ago [-]
> These metrics definitely are debatable (they were made in the 70’s…)

What is it about a decade that makes contributions produced thereabout "debatable"?

russelg 2 hours ago [-]
I believe their point is less about the specific decade, but rather they were made over 50 years ago.
AnimalMuppet 1 hours ago [-]
In 50 years, we've learned some things. Not all "good advice for programming" from the 1970s is still actually good advice.
jedisct1 2 hours ago [-]
Rust.
0xbadcafebee 2 hours ago [-]
I just wanted to point out that "Cognitive Complexity" [1] was not invented by SonarSource, it is an academic principle created in the 1950s and has more to do with psychology than computer science. Computer science has over-simplified the term to mean "hey there's a lot of stuff to remember this is hard".

Psychology tends to have a wider scope of thought and research put into it [2] [3]. For example, one way it's used is not to measure how complex something is, but how capable one particular person is at understanding complex things, versus a different human [4]. This can affect everything from leadership decisions [5] to belief in climate change [6].

I point this out because all too often Engineers hyper-focus on technical details and forget to step back and consider a wider array of factors and impacts - which, ironically, is what cognitive complexity is all about. It's the ability of a person to think about more things in a deeper way. Basically, cognitive complexity is a way to talk about not just things, but people.

We also have a tendency as Engineers to try to treat everyone and everything as a blob. We have to design our language in X way, because all people supposedly work in the same way, or think the same way. Or we have to manage our code in a certain way, because all the team members are assumed to work better that way (usually in whatever way is either easier or simpler).

One thing I wish people would take away from this, is that not only is cognitive complexity actually useful (it describes how language is able to work at all), but some people are better at it than others. So "avoiding cognitive complexity" is, in many ways, a bad thing. It's like avoiding using language to convey ideas. Language and communication is hard, but you're reading this right now, aren't you? Would you rather a pictogram?

[1] https://en.wikipedia.org/wiki/Cognitive_complexity [2] https://www.jstor.org/stable/2785779 [3] https://pubmed.ncbi.nlm.nih.gov/11014712/ [4] https://testing123.education.mn.gov/cs/groups/educ/documents... [5] https://deepblue.lib.umich.edu/handle/2027.42/128994 [6] https://www.sciencedirect.com/science/article/abs/pii/S02724...

shortrounddev2 4 hours ago [-]
There's a cool plugin for vscode called Highlight[1] that lets you set custom regexes to apply different colors to your code. I think a common use of this is to make //TODO comments yellow, but I use it to de-emphasize logs, which add a lot of visual noise because I put them EVERYWHERE. The library I maintain uses logs that look like:

    this.logger?.info('Some logs here');
So I apply 0.4 opacity to it so that it kind of fades into the background. It's still visible, but at a glance, the actual business logic code pops out at you. This is my configuration for anyone who wants to modify it:

    //In vscode settings.json:
    "highlight.regexes": {
        "((?:this\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)": {
            "regexFlags": "gmi",
            "decorations": [{
                "opacity": "0.4"
            }]
        }
    },
---

[1] https://marketplace.visualstudio.com/items?itemName=fabiospa...

satisfice 4 hours ago [-]
Interesting how important mere opinion seems to be, because the author doesn't seem to mention two issues that matter a great deal to me:

- Alignment of braces and brackets, instead of an opening brace at the end of one line and the closing brace at the beginning of a subsequent line. - everything I need to see is within an eyespan, instead having to jump to several different files to trace code.

deadbabe 4 hours ago [-]
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 time is really just for noobs who don’t have great code comprehension skills and appreciate the pause to have a chance to think. If the variable’s purpose for existence is just to get passed on to a next function immediately, it shouldn’t exist at all. Learn to lay pipe.

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact
Rendered at 18:04:38 GMT+0000 (Coordinated Universal Time) with Vercel.