[HN Gopher] What makes code hard to read: Visual patterns of com...
___________________________________________________________________
What makes code hard to read: Visual patterns of complexity (2023)
Author : homarp
Score : 244 points
Date : 2025-03-11 10:07 UTC (12 hours ago)
(HTM) web link (seeinglogic.com)
(TXT) w3m dump (seeinglogic.com)
| Pannoniae wrote:
| 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 wrote:
| > 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.
| _dark_matter_ wrote:
| 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 wrote:
| What are talking about?! That's my favourite part when
| reading code! :P
| dijksterhuis wrote:
| > [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 wrote:
| 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.
| robert_dipaolo wrote:
| 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.
| jodrellblank wrote:
| > 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.
| mpweiher wrote:
| 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.
| jghn wrote:
| 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.
| hobs wrote:
| 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.
| michaelcampbell wrote:
| > 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 wrote:
| I think the whole Uncle Bob clean code movement has a lot to
| answer for.
| bluGill wrote:
| 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.
| dsego wrote:
| https://github.com/johnousterhout/aposd-vs-clean-code
| cardanome wrote:
| 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.
| jt2190 wrote:
| 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.
| usrbinenv wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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.
| alpinisme wrote:
| 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 wrote:
| 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.
| James_K wrote:
| 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.
| BorgHunter wrote:
| 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 wrote:
| That is exactly what is discussed in:
|
| https://www.goodreads.com/book/show/39996759-a-philosophy-of...
| dsego wrote:
| Man, this is the third reference to this book I am seeing
| this week, I need to order this book.
| WillAdams wrote:
| Gave away a copy to a developer in Brazil, and ordered the
| Kindle version, and need to order another print copy.
|
| Can't recommend it highly enough --- I found it
| transformative --- read through it one chapter at a time,
| then re-worked the code of my current project:
|
| https://github.com/WillAdams/gcodepreview
|
| then went on to the next chapter --- about the halfway
| point I had the code cleaned up so well that the changes
| became quite minor.
| bluGill wrote:
| 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.
| isleyaardvark wrote:
| I'd add that I follow that approach because the optimizers are
| more likely to optimize readable code than weird hacks.
| evnp wrote:
| 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 wrote:
| 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.
| evnp wrote:
| Agree with you completely. I'm not against intermediate
| variables (though I tend to appreciate the way comments
| separate context from code, more than my hardline "code
| should be self-documenting" colleagues do). But I don't
| think they should come at the cost of textual clarity.
|
| I think you could look at this through a "dimensional"
| lens. I'm ok with linear dot chains (or even better, pipe
| chains) - you read operations top to bottom. I'm also ok
| with single line chains where they fit, especially when
| contained neatly in a single-line function - in this case
| operations read left to right. But the second form in this
| example forces you to read operations top-to-bottom AND
| left-to-right at once, creating a 2-dimensional "wall of
| noise" effect for me. I'd expect the issues compound as ops
| are added, instead of increasing linearly with chain
| syntax. All very subjective and familiarity-dependent of
| course.
| superjan wrote:
| I do both. It depends on whether I can think of a concise
| variable name that faithfully describes the intermediate
| result. If you need more than 20-ish characters to describe
| it, then it is better to leave it chained.
| James_K wrote:
| 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 wrote:
| 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).
| WillAdams wrote:
| 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 wrote:
| 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 wrote:
| 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_ wrote:
| 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 wrote:
| Thanks! I've added that to the list.
|
| I'd be grateful of any other such texts.
| donatj wrote:
| Your link for literate programming just gets me a Parallels
| H-Sphere error
| WillAdams wrote:
| Try searching for "literate programming" --- it should be the
| top link.
| recursivedoubts wrote:
| 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.
| zwnow wrote:
| 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.
| ajross wrote:
| > 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.
| zwnow wrote:
| 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 wrote:
| > 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 wrote:
| 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.
| whstl wrote:
| _> 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_
|
| That's also my impression and experience.
|
| And sometimes there is no problem at all, but
| abstractions are still pushed too far and then a problem
| arises in the form of non-essential complexity.
| necrotic_comp wrote:
| 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 wrote:
| > 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.
| jacobr1 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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
| horsawlarway wrote:
| 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.
| apelapan wrote:
| 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.
| skydhash wrote:
| > _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?_
|
| All of this.
| jajko wrote:
| 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
| sunrunner wrote:
| 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 wrote:
| 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 wrote:
| > 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 wrote:
| My original comment never was about understanding the
| implementation details. It was about understanding the
| high level goal of the function.
| andybp85 wrote:
| 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 wrote:
| 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?
| brulard wrote:
| 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.
| quinnirill wrote:
| Yeah, I agree in spirit but I think the answer is more
| "it depends" than something where you should feel bad or
| something if you deviate from it. If washDishes also
| sends a bunch of metrics/diagnostics or updates a
| database of your favorite dish washing programs somewhere
| inside it, that's probably fine. Otherwise you push the
| path of least resistance to just be vague instead, then
| you get a codebase full of functions with names like
| handle or process.
| jbeninger wrote:
| 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.
| skydhash wrote:
| Some function can be named generally but the name is
| specific when concatenated with the module name and/or
| the package name. `load` is generic, but
| `app.config.load` is more descriptive.
| intrasight wrote:
| > Doesn't matter how it looks.
|
| That's the mindset that the author is trying to counter.
| callc wrote:
| 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.
| jayd16 wrote:
| 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?
| jimmaswell wrote:
| 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...
| fasbiner wrote:
| 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.
| bad_haircut72 wrote:
| Do you think you would understand every function in the doom
| codebase in under 5 seconds? Is this bad proframming then?
| User23 wrote:
| Mathematicians have recognized the importance of elegance for
| millennia.
| louthy wrote:
| 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!)
| qwertygnu wrote:
| > 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.
|
| Is this just a fancy way of saying static functions?
| louthy wrote:
| 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 declaratively represented by
| something like an IO monad. Which in reality is just a
| lambda with the side-effecting behaviour in the body of the
| lambda.
|
| So, to make a pure IO function you don't actually perform
| the IO in the function, you return a data type (the lambda)
| that represents the IO to perform. This maintains the
| purity if the function and 'passes the buck' to the caller.
| In the case of Haskell, all the way up to its Main function
| and into its runtime -- making the language itself pure,
| even if the runtime isn't.
|
| This isn't just a Haskell thing though. I'll write code
| this way in C# (and have built a large pure-FP framework
| for C# to facilitate this approach [1]).
|
| Here's an example of the more 'narrative style' [2] of C#
| using pure-FP. It reads from top-to-bottom, keeping the
| related functions near each other and walking the reader
| through the functionality. There's also a massive removal
| of the usual clutter you see in C#/Java programs, getting
| down to the essence of the logic. It won't be to
| everybody's taste (as it's not idiomatic at all), but it
| demonstrates the idea.
|
| This style works well for regular program logic and less
| well for things like APIs where there's not always a
| narrative you can tell.
|
| [1] https://github.com/louthy/language-ext
|
| [2] https://github.com/louthy/language-
| ext/blob/main/Samples/Car...
| syklemil wrote:
| > 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 a large pure-FP framework
| for C# to facilitate this approach [1]).
|
| I think that habit from Haskell is also what allowed me
| to pick up Rust pretty easily. You don't run afoul of the
| borrowchecker much if you don't expect to mutate a lot of
| stuff, and especially at a distance.
| chowells wrote:
| 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.
| syklemil wrote:
| Right you are. I wish I had an excuse for my mistake, but
| I don't.
| WillAdams wrote:
| Arguably, this is why Literate Programming (see my comment
| elsethread) didn't take off.
| freetonik wrote:
| >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 wrote:
| 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 wrote:
| 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)
| gwbas1c wrote:
| 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.)
| mannykannot wrote:
| 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 wrote:
| 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.
| mannykannot wrote:
| 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.
| ledauphin wrote:
| _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 wrote:
| 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.
| deadbabe wrote:
| 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.
| __mharrison__ wrote:
| I agree with you on piping but writing each method on its own
| line makes the code very approachable (also easier to work
| with).
|
| Consider this code (from a course I'm teaching this week):
| (df .pipe(lambda df_: print(df_.columns) or df_)
| .groupby('activity_id', observed=True) [non_agg_cols]
| .apply(lambda g: g.assign(distance=calculate_distance_np(g)),
| include_groups=True) .pipe(fix_index)
| .pipe(lambda df_: print('DONE!') or df_) )
|
| vs: (df.pipe(lambda df_: print(df_.columns)
| or df_).groupby('activity_id', observed=True)
| [non_agg_cols].apply(lambda g:
| g.assign(distance=calculate_distance_np(g)),
| include_groups=True).pipe(fix_index).pipe(lambda df_:
| print('DONE!') or df_))
| deadbabe wrote:
| I don't really think the first example is easier to read,
| it's more of an illusion. A skilled reader should be able to
| carry the context as they read along. It is only because we
| occasionally work with inexperienced coders that the list
| style is necessary. Consider:
|
| "The quick brown fox jumped over the lazy ass dog"
|
| Vs
|
| The quick brown fox
|
| jumped over
|
| the lazy ass dog
|
| The second example helps a reader understand the subjects and
| action but it is wholly unnecessary for people who know how
| to read.
| __mharrison__ wrote:
| It is certainly easier to work with the former. If you need
| to comment out a line, it is painless.
| mrkeen wrote:
| 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 wrote:
| > 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 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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)
| shortrounddev2 wrote:
| 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": { "((?:th
| is\\.)?(?:_)?logger(?:\\?)?.(debug|error|info|warn)[^\\)]*\\)\\;)
| ": { "regexFlags": "gmi",
| "decorations": [{ "opacity": "0.4"
| }] } },
|
| ---
|
| [1]
| https://marketplace.visualstudio.com/items?itemName=fabiospa...
| fs_software wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| 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 wrote:
| For anyone interested in this as design, it's called method
| chaining.
| closed wrote:
| 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 wrote:
| Good to know. I assumed it was all done via objects or
| things like objects.
|
| So is piping more functional programming?
| closed wrote:
| 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 wrote:
| Ok, that's helpful. Thanks!
| __mharrison__ wrote:
| I wrote a book dedicated to writing Pandas code in this
| style, Effective Pandas 2.
|
| I've seen many who complain about this style of coding, but
| once they try it, they are sold. I love reading reviews about
| how adopting this made their code easier to write, debug,
| read, and collaborate.
| satisfice wrote:
| 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.
| stared wrote:
| 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.
| CatAtHeart wrote:
| 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 wrote:
| 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.
| __oh_es wrote:
| I feel guard clauses/early returns end up shifting developer
| focus on narrowing the function operation, and not an
| originally happy path with some after thought about other
| conditions it could handle.
|
| IME else's also end up leading to further nesting and
| evaluating edge cases or variables beyond the immediate scope
| of happy path (carrying all that context!).
| brulard wrote:
| 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.
| bogomog wrote:
| 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!
| culopatin wrote:
| While this is simple and all, the English words if/else don't
| require the reader to know the ?: convention. Depending on
| what background the reader may have, they could think of the
| set notation where it could mean "all the evens such that odd
| is true" which makes no sense. Its also very close to a
| key:value set notation. If/else leave no doubts for the
| majority of readers. It's more inclusive if you will.
| bogomog wrote:
| That's why I gave the caveat that if using a language with
| the ternary operator, one should know that operator. Python
| tried using English words for a ternary, but I think that's
| awkward from a readability perspective. A limited set of
| symbolic syntax improves readability over using words in my
| opinion, there's less text to scan.
| ajuc wrote:
| I love code golf as much as anyone, not sure it's worth it on
| such small methods tho. Any of the propositions would be
| fine. Anyway: def oddness(n):
| return ["Even", "Odd"][n % 2]
|
| BTW this trick with replacing if-then-else with a lookup is
| sometimes very useful. Especially if there's many ifs.
| stared wrote:
| This is, IMHO, the idiomatic way to do so.
| zoogeny wrote:
| 90% of the time I prefer the first. I am allergic to
| indentation and I hate anything remotely like:
| function foo(a) { if (a) { return
| doThing() } else { return Error();
| } }
|
| I like all of my assertion and predicate guards nicely at the
| top of a function: function foo(a) {
| if (!a) { return Error() }
| return doThing() }
|
| And for that reason, I would probably go for getOddness4 even
| though I see your point.
| Izkata wrote:
| 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.
| jraph wrote:
| 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 :-)
| 12_throw_away wrote:
| 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 ...
| Izkata wrote:
| AST level would have to automatically figure out what parts
| should be aligned, an alternative is to keep saving it in
| text but tweak the meaning of the "tab" character, so the
| developer still has control over what gets aligned:
| https://nick-gravgaard.com/elastic-tabstops/
|
| Not great since viewing it in something that doesn't
| understand elastic tabstops would just be a mess, but it
| solves one of the issues the other response brings up, and I
| think some sort of user control like that is going to remain
| necessary either way.
| gwbas1c wrote:
| > 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.
| superjan wrote:
| I agree. After compiling it is even quite likely that the
| compiler does not care you gave a name to a return value
| (assuming you let it infer the variable type). What you will
| see in practice is that the intermediate is explicitly
| "materialized" (e.g. into a list), because the author wanted to
| inspect it in the debugger. That will have some cost, mostly in
| the form of avoidable allocations.
| dchristian wrote:
| 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.
| morning-coffee wrote:
| > 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 wrote:
| I believe their point is less about the specific decade, but
| rather they were made over 50 years ago.
| AnimalMuppet wrote:
| In 50 years, we've learned some things. Not all "good advice
| for programming" from the 1970s is still actually good advice.
| gwbas1c wrote:
| > 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._
| slevis wrote:
| 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.
| marcosdumay wrote:
| 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.
| Jensson wrote:
| 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 wrote:
| _> 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.
| stopthe wrote:
| 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.
| 0xbadcafebee wrote:
| 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...
| moi2388 wrote:
| 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
| jedisct1 wrote:
| Rust.
| feoren wrote:
| > 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 wrote:
| authors_of_long_books = set() for book in books:
| if len(book.pages) > 1000:
| authors_of_long_books.add(book.author) return
| authors_of_long_books
|
| You are told explicitly at the beginning what the type of the
| result will be, you see that it's a single pass over books and
| that we're matching based on page count. There are no
| intermediate results to think about and no function call
| overhead.
|
| When you read it out loud it's also it's natural, clear, and in
| the right order-- "for each book if the book has more than 1000
| pages add it to the set."
| syklemil wrote:
| 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 wrote:
| 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.
| xen0 wrote:
| Set comprehensions are normal in mathematics and, barring
| very long complex ones, I find them the easiest to parse
| because they are so natural.
|
| They're just a tad more verbose in Python than
| mathematics because it uses words like 'for' and 'in'
| instead of symbols.
| nicwolff wrote:
| authors_of_long_books: set[Author] = {
| book.author for book in books
| if book.page_count > 1000 }
| syklemil wrote:
| You've had some answers already, but I also think this is
| a good argument for syntax highlighting. With tools like
| tree-sitter it's pretty easy these days to get high
| quality syntax highlighting, which allows us humans to
| receive more information in parallel. A lot of the
| information we pick up in our daily lives is carried
| through color, and being colorblind is generally seen as
| a disability (albeit often a mild one which can be
| undetected for decades).
|
| Syntax highlighting in print is more limited because of
| technological and economic constraints, which might leave
| just bold, italics and underlines on the table, while
| dropping color. On screens and especially in our editors
| where we see the most code, a lack of color is often a
| self-imposed limitation.
| davidw wrote:
| That's not the point though. If you need the syntax
| highlighting to quickly make out the structure, perhaps
| the visual layout is not as good as it could be.
| syklemil wrote:
| I consider syntax highlighting to be a part of the
| _visual_ structure. Visibility is more than just
| whitespace and placement!
| d0mine wrote:
| Set comprehension are more idiomatic here (explicit syntax)
| though filter/map are not that bad too:
| {*map(_.author, filter(_.page_count > 1000, books))}
|
| It uses lambdas package.
| itsmeknt wrote:
| 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 favoriteFoodsOfFurry
| PetsOfFamousAuthorsOfLongChineseBooksAboutHistory = books
| .filter(book => book.pageCount > 100 and
| book.language == "Chinese" and book.subject ==
| "History" and book.author.mentions > 10_000
| ) .flatMap(book => book.author.pets)
| .filter(pet => pet.is_furry) .map(pet =>
| pet.favoriteFood) .distinct() #
| Procedural approach var favoriteFoodsOfFurryPe
| tsOfFamousAuthorsOfLongChineseBooksAboutHistory = set()
| for book in books: if len(book.pageCount > 100) and
| book.language == "Chinese" and book.subject ==
| "History" and book.author.mentions > 10_000:
| for pet in book.author.pets: if pet.is_furry:
| favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAb
| outHistory.add(pet.favoriteFood) # Comprehension
| approach var favoriteFoodsOfFurryPetsOfFamousA
| uthorsOfLongChineseBooksAboutHistory = {
| pet.favoriteFood for pet in pets for pets in
| [book.author.pets for book in books if
| len(book.pageCount > 100) and
| book.language == "Chinese" and
| book.subject == "History" and
| book.author.mentions > 10_000] if pet.is_furry
| }
|
| FWIW, for more complex problems, I think the second one is
| the most readable.
| syklemil wrote:
| I'm more partial to the first one because it keeps a
| linear flow downwards, and a uniform structure. The
| second one kind of drifts off, and reshuffling parts of
| it is going to be ... annoying. IME the dot style lends
| itself much better to restructuring.
|
| Depending on language you might also have some
| `.flat_map` option available to drop the `.reduce`.
| itsmeknt wrote:
| True! Good point on the restructuring, I haven't thought
| about it in that way.
|
| I think I like the second approach because the loop
| behavior seems clearest, which helps me analyze the time
| complexity or when I want to skim the code quickly.
|
| A syntax like something below would be perfect for me if
| it existed: var favoriteFoodsOfFurryPetsO
| fFamousAuthorsOfLongChineseBooksAboutHistory =
| books[i].author.pets[j].favoriteFood.distinct()
| where i = pagecount > 100, language ==
| "Chinese", subject == "History",
| author.mentions > 10_000 where j = is_furry ==
| True
| tsss wrote:
| Your FP example is needlessly complicated. No one who
| does FP regularly would write it like that.
| var favoriteFoodsOfFurryPetsOfFamousAuthorsOfLongChineseB
| ooksAboutHistory = 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()
|
| Or in Scala: val favoriteFoodsOfFurryPets
| OfFamousAuthorsOfLongChineseBooksAboutHistory = (for {
| book <- books if book.pageCount > 100 &&
| book.language == "Chinese" && book.subject ==
| "History && book.author.metnions > 10_000
| pet <- book.author.pets if pet.is_furry }
| yield pet.favoriteFood).distinct
|
| Though, most Scala programmers would prefer higher-order
| functions over for-comprehensions for this.
| itsmeknt wrote:
| 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!
| Chris_Newton wrote:
| I didn't see the original, but the FP example here looks
| fairly idiomatic to me.
|
| An alternative, which in FP-friendly languages would have
| almost identical performance, would be to make the shift
| in objects more explicit: var favoriteF
| oodsOfFurryPetsOfFamousAuthorsOfLongChineseBooksAboutHist
| ory = books .filter(book =>
| isLongChineseBookAboutHistory(book))
| .map(book => book.author) .filter(author =>
| isFamous(author)) .flatMap(author =>
| author.pets) .filter(pet => pet.isFurry)
| .map(pet => pet.favouriteFood) .distinct()
|
| I slightly prefer this style with such a long pipeline,
| because to me it's now built from standard patterns with
| relatively simple and semantically meaningful
| descriptions of what fills their holes. Obviously there's
| some subjective judgement involved with anything like
| this; for example, if the concept of an author being
| famous was a recurring one then I'd probably want it
| defined in one place like an `isFamous` function, but if
| this were the only place in the code that needed to make
| that decision, I might inline the comparison.
| tremon wrote:
| _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 wrote:
| _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 wrote:
| You are describing _how_ to execute the procedure, while
| the gp is describing _what_ the result should be. Both
| are valuable, but they 're very different.
|
| My personal take is that "how to execute" is more useful
| for lower level and finer grained control, which "what
| the results should be" is better for wrangling complex
| logic
| skydhash wrote:
| That's when you're doing the job, not what the mental
| representation of the solution. I strongly believe if you
| ask her to describe the task, she would go:
|
| 1. (Take the books)->(that have 200 pages or more)->(and
| mark down the name of the authors)->(only once)
| bdangubic wrote:
| I respectfully disagree. And I think one of the core
| reason SWEs struggle with functional-style of programming
| is that it is neither intuitive nor how general-joe-doe's
| brain works.
| whstl wrote:
| I haven't really encountered software engineers who
| really struggle with functional style in almost 20 years
| of seeing it in mainstream languages. It's just another
| tool that one has to learn.
|
| Even the people arguing against functional style are able
| to understand it.
|
| Strangely, this argument is quite similar to arguments I
| encounter when someone wants to rid the codebase of all
| SQL and replace it with ORM calls.
| bdangubic wrote:
| _Strangely, this argument is quite similar to arguments I
| encounter when someone wants to rid the codebase of all
| SQL and replace it with ORM calls._
|
| we must be in completely different worlds cause I have
| yet (close to 30 years now hacking) to see/hear someone
| trying to introduce ORM on a project which did not start
| with the ORM to begin with. the opposite though is a
| constant, "how do we get rid of ORM" :)
|
| _I haven 't really encountered software engineers who
| really struggle with functional style in almost 20 years.
| It's just another tool that one has to learn._
|
| I recall vividly when Java 8 came out (not the greatest
| example but also perhaps not too bad) having to explain
| over and over concept of flatMap (wut is that fucking
| thing?) or even zipping two collections. even to this day
| I see a whole lot of devs (across several teams I work
| with) doing "procedural" handling of collections in for
| loops etc...
| whstl wrote:
| I'm more talking about projects that do start with an
| ORM, but have judicious (and correct) usage of inline SQL
| for certain parts. It's not uncommon to see developers
| spending weeks refactoring into an ORM-mess.
|
| The argument is always that "junior developers won't know
| SQL".
|
| But yeah I've also seen the opposite happening once.
| People going gung-ho on deleting all ORM code "because
| there's so much SQL already, why do we need an ORM then".
|
| And then the argument is that "everyone knows SQL, the
| ORM is niche".
|
| I guess it's a phase that all devs go through in the
| middle of their careers. They see a hammer and a
| screwdriver in a toolbox, and feel the need for throwing
| one away because "who needs more than one tool"...
| tvier wrote:
| 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
| khaledh wrote:
| That's not true. The lambdas used in the functional version
| are each called once for every item in the list.
| stouset wrote:
| No sane optimizer is going to emit the functional code as
| a gajillion function calls.
| khaledh wrote:
| True, but now you're relying on a specific implementation
| and optimization of the compiler, unless the language
| semantics explicitly say that lambdas will be inlined.
| stouset wrote:
| This is true of literally anything and everything your
| compiler emits. In practice the functional style is
| _much_ easier to optimize to a far greater degree than
| the imperative style.
| tvier wrote:
| This is why you shouldn't get into arguments about
| performance on the internet without highly specified
| execution environments.
|
| I'm going to take my own advice and go back to work :)
| tvier wrote:
| Yeah, if you treat it as javascript vs python they're
| likely correct (I'm not that familiar with js). The
| article and original comment were about function vs
| imperative though, so I assumed half decent runtimes for
| both.
| Spivak wrote:
| It's not? How could that possibly work when the lambda
| could throw and it could throw on the nth invocation and
| your stack trace has to be correct?
|
| If I run this in the JS console I get two anonymous stack
| frames. The first being the console itself.
| [1, 2, 3].filter(x => [][0]())
| feoren wrote:
| > You are told explicitly at the beginning what the type of
| the result will be
|
| I would argue that's a downside: you have to pick the
| appropriate data structure beforehand here, whereas
| .distinct() picks the data structure for you. If, in the
| future, someone comes up with a better way of producing a
| distinct set of things, the functional code gets that for
| free, but this code is locked into a particular way of doing
| things. Also, .distinct() tells you explicitly what you want,
| whereas the intention of set() is not as immediately obvious.
|
| > There are no intermediate results to think about
|
| I could argue that there aren't really intermediate results
| in my example either, depending on how you think about it.
| Are there intermediate results in the SQL query "SELECT
| DISTINCT Author FROM Books WHERE Books.PageCount > 1000"?
| Because that's very similar to how I mentally model the
| functional chain.
|
| There are also intermediate results, or at least intermediate
| state, in your code: at any point in the loop, your set is in
| an intermediate state. It's not a big deal there either
| though: I'd argue you don't really think about that state
| either.
|
| > and no function call overhead
|
| That's entirely a language-specific thing, and volatile: new
| versions of a language may change how any of this stuff is
| implemented under the hood. It could be that "for ... in"
| happens to be a relatively expensive construct in some
| languages. You're probably right that the imperative code is
| slightly faster in most languages today, and if it has been
| shown via performance analysis that this particular code is a
| bottleneck, it makes sense to sacrifice readability in favor
| of performance. But it _is_ a sacrifice in readability, and
| the current debate is over which is more readable in the
| first place.
|
| > a single pass over books
|
| Another detail that may or may not be true, and probably
| doesn't matter. The overhead of different forms of loops is
| just not what's determining the performance of almost any
| modern application. Also, my example _could_ be a single pass
| if those methods were implemented in a lazy, "query builder"
| form instead of an immediately-evaluated form.
|
| In fact, whether this query should be immediately evaluated
| is not necessarily this function's decision. It's nice to be
| able to write code that doesn't care about that. My example
| works the same for a wide variety of things that "books"
| could be, and the strategy to get the answer can be different
| depending on what it is. It's possible the result of this
| code is exactly the SQL I mentioned earlier, rather than an
| in-memory set. There are lots of benefits to saying _what_
| you want, instead of specifying exactly _how_ you want it.
| megous wrote:
| Set is a well defined container for unique values. It's
| much clearer what it is than some non-existent .distinct()
| function with no definition and unclear return value.
|
| Procedural code in JS doesn't say how you want something
| done any more closely than the functional style variant.
| for-of is far more generic than .map/.filter() since .map()
| only works on Array shaped objects, and for-of works on all
| iterables, even generators, async generators, etc. In any
| case you're not saying how the iteration will happen with
| for-of, you're just saying that you want it. Implementation
| of Set is also the choice of a language runtime. You're
| just stating what type of container you want.
|
| Sometimes functional style may be more readable, sometimes
| procedural style may.
| jltsiren wrote:
| Your example is a conceptually simple filter on a single list
| of items. But once the chain grows too long, the conditions
| become too complex, and there are too many lists/variables
| involved, it becomes impossible understand everything at once.
|
| In a procedural loop, you can assign an intermediate result to
| a variable. By giving it a name, you can forget the processing
| you have done so far and focus on the next steps.
| stouset wrote:
| You don't ever need to "understand everything at once". You
| can read each stanza linearly. The _for loop style_ is the
| approach where everything often needs to be understood all at
| once since the logic is interspersed throughout the entire
| body.
| jltsiren wrote:
| In the example above, you first have a list of books. Then
| you filter it down to books with >1000 pages. Then you map
| it to authors of books with >1000 pages. Then you collapse
| it to distinct authors of books with >1000 pages. Every
| step in the chain adds further complexity to the
| description of the things you have, until it exceeds the
| capacity of your working memory. Then you can no longer
| reason about it.
|
| The standard approach to complexity like that is to invent
| useful concepts and give them descriptive names. Then you
| can reason about the concepts themselves, without having to
| consider the steps you used to reach them.
| lelandbatey wrote:
| 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 chain in isolation, as its
| inputs are the prior link and its outputs will be used in
| the next link. Giving a name to each one of those links
| in the chain isn't always necessary, and depending on how
| trivial the operations are, can really hurt readability.
|
| I think its very much a personal preference.
| jltsiren wrote:
| 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 going on.
|
| You don't have to give a name to every intermediate
| state, just like you don't have to comment every single
| line of code. But sometimes the names and comments do
| improve readability.
| stouset wrote:
| 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.
| bluGill wrote:
| 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 some poor person who has never
| seen this code before.
| stouset wrote:
| 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.
| solid_fuel wrote:
| 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.
| yxhuvud wrote:
| 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.
| stouset wrote:
| You have literally just described the set of objects
| asked for: the unique authors of the books with more than
| 1,000 pages. I don't understand how you expect to get any
| simpler than that. The functional style isn't even
| requiring you to describe how to accomplish it, it almost
| verbatim simply describes the answer you're trying to
| get.
|
| If your entire objection is that you might want
| intermediate-named variables... you can just do that?
| var longBooks = books.filter(book => book.pageCount
| > 1000) var authors = longBooks.map(book
| => book.author) var distinctAuthors =
| authors.distinct()
|
| For short chains (95%+ of cases), this is far more mental
| overhead. For the remaining cases, you can just name the
| parts? I'm just completely failing to see your problem
| here.
| jltsiren wrote:
| The problem is that it's easy to overdo it. When you are
| writing the code, you already know what it's supposed to
| do, and adding a few more things to the chain is
| convenient and attractive. But when you are reading
| unfamiliar code, you often wish that the author was more
| explicit with their code. Not just with what the code is
| actually doing, but what it's trying to do and what are
| the key waypoints to get there.
|
| With procedural code, it's widely accepted that you
| should not do too many things in a single statement. But
| in functional code, the entire chain is a single
| statement. There are no natural breakpoints where the
| reader could expect to find justifications for the code.
| stouset wrote:
| > The problem is that it's easy to overdo it.
|
| Welcome to all features of every programming language?
|
| Sacrificing readability, optimization, and simplicity for
| the 95% case because some un-principled developers might
| overdo it in the 5% case (when the cost of fixing it is
| trivially just inserting variable assignments) is... not
| a good trade-off.
| jltsiren wrote:
| 5% is common enough that you'll encounter it almost every
| time you read code. And fixing it is not easy, because
| you first need to understand the code before you can add
| useful variable names.
|
| Besides, programming language evolution is mostly driven
| by the fact that everyone is lazy and unprincipled at
| least occasionally. If you need to be disciplined to
| avoid footguns, you'll trigger them sooner or later.
| stouset wrote:
| The cost of this "footgun" is basically zero. Every step
| in a functional pipeline is isolated and wholly
| independent. If you want to split such a pipeline in two,
| doing so is trivial.
| whstl wrote:
| 5% is also low enough that you can just use another
| technique for the exceptions.
| whstl wrote:
| _> But in functional code, the entire chain is a single
| statement_
|
| Not necessarily. You can use intermediate variables when
| necessary.
| __mharrison__ wrote:
| It's also harder to write and debug with the intermediate
| steps.
| nomel wrote:
| How so? The states of the intermediate steps are
| logically and easily exposed in a debugger. You can also
| easily set conditional breakpoints relative to the
| intermediate states.
|
| I know that intermediate states are generally easier to
| comprehend, because I never have to explain them in code
| reviews. To avoid having to explain chains to others, I
| end up having to add descriptive comments to the
| intermediate steps, far exceeding the number of
| characters the descriptive intermediate variables would
| take. That's why I avoid them, or break them up: time
| spent in code reviews has proven to me that people have
| trouble with chains.
| __mharrison__ wrote:
| Build up and debug the chain as you work in an
| environment like Jupyter. No need to create variables.
| Just run the code and verify that the current step works.
| Then, proceed to the next. Then, put the chain in a
| function. If you want to be nice, put a .loc as the first
| step to explicitly list all of the input columns. Drop
| another .loc as the last step to validate the output
| columns. (This also serves as a test and documentation to
| future you about what needs to come in and out.) Create a
| simple unit test with a sample of the data if you desire.
|
| I've found that the constraint of thinking in chains
| forces me to think of the recipe that I need for my data.
| Of course, not everything can be done in a stepwise
| manner (.pipe helps with that), but often, this
| constraint forces you to think about what you are doing.
|
| Every good Pandas user I know uses it this way. I've
| taught hundreds more. Generally, it feels weird at first
| (kind of like whitespace in Python), but after a day, you
| get used to it.
|
| Do you store intermediate results of SQL?
| ffsm8 wrote:
| That's only true for casual reviewing and writing.
|
| When you're actually analyzing a bug, or need to add a new
| feature to the code... Then you'll have to keep the whole
| thing in your mind. No way around it
|
| It gets extra annoying when people have complex maps,
| reduces, flat maps all chained after the next, and each
| step moved into a named function.
|
| HF constantly jumping around trying to rationalize why
| something happens with such code...
|
| It looks good on first glance, but it inevitably becomes a
| dumpster fire as soon as you need to actually interact with
| the code.
| __mharrison__ wrote:
| This. I teach this with Pandas (and Polars) all the time.
| You don't really care about the intermediate values. You
| build up the chain operation by operation (validating that
| it works). At the end you have a recipe for processing the
| data.
|
| Most professional Pandas users realize that working with
| chains makes their lives much easier.
|
| By the way, debugging chains isn't hard. I have a chapter
| in my book that shows you how to do it.
| reubenmorais wrote:
| In a practical example you'd create a named intermediate type
| which becomes a new base for reasoning. Once you convinced
| yourself that the first part of the chain responsible for
| creating that type (or a collection of it) is correct, you
| can forget it and free up working memory to move on to the
| next part. The pure nature of the steps also makes them
| trivially testable as you can just call them individually
| with easy to construct values.
| kccqzy wrote:
| If you assign an intermediate result to a variable in a
| procedural loop, you can also assign intermediate results of
| parts of this chain to variables.
| titzer wrote:
| SELECT DISTINCT author FROM books WHERE pageCount > 1000;
| __mharrison__ wrote:
| Folks don't seem to have a problem when SQL does it. Only
| when code like Pandas does it...
| beryilma wrote:
| This is 5 times more readable than FP example above for the
| same computation. The FP example uses variable book(s) five
| times, where using it once was sufficient for SQL. Perhaps FP
| languages could have learned something from SQL...
| YesBox wrote:
| Scrolled to find the SQL. Such an elegant, powerful language.
| Really happy I chose SQLite for my game/project.
| dsego wrote:
| That's not a long chain. It doesn't even have a reduce, try
| nesting a few reducers and see how you like it.
| aaronbrethorst wrote:
| What is "long"?
| seeinglogic wrote:
| 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, which is the length of the example I
| used.
|
| For the meaning of "multiple", I intended that to mean if there
| are nested chains or if the type being operated on changes,
| that can slow down the rate of reading for me.
|
| Functional programming constructs can be very elegant, but it's
| possible to go overboard :)
| ninetyninenine wrote:
| >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 < x.length; x++) { value = x[i] + 5
| value += 5 stringValue = str(value).concat(hello)
| acc += stringValue + "," } for (var i = 0,
| i < acc.length; i++) { acc[i] =
| capitalLetter(acc[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.
| harrison_clarke wrote:
| you have this backwards: reusing code couples the code.
| copy+paste uncouples code
|
| if you have two functions, they're not coupled. you change
| one, the other stays as-is
|
| if you refactor it so that they both call a third function,
| they're now coupled. you can't change the part they have in
| common without either changing both, or uncoupling them by
| duplicating the code
|
| (you often want that coupling, if it lines up with the
| semantics)
| feoren wrote:
| 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.
| desumeku wrote:
| o_node := graph.GetNodeByName(name) var ret []string
| for _, node := range o_node.connectedNodes() { if
| !node.isHidden { ret = append(ret, node.name)
| } } return ret
| climb_stealth wrote:
| Just want to add that I both agree with you and parent.
| Your examples are readable and I see no issues there.
|
| It might be a language thing as well. In Python often
| people take list-comprehensions too far and it becomes an
| undecipherable mess of nested iterators, casts and lists.
|
| There are always exceptions :)
| yongjik wrote:
| 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?
| yxhuvud wrote:
| 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.
| mrkeen wrote:
| The best way to kick the tyres on this kind of question is to
| plug in something literally infinite. That way if you arrive
| at an answer you're probably doing something right with
| regard to space and time usage.
|
| For example, use all the prime numbers as an expression in
| your chain. import Data.Function
| import Data.Numbers.Primes main = do
| let result :: [Int] = primes
| & filter (startingWithDigit '5')
| & asPairs & map pairSum
| & drop 100000 & take 10
| print result asPairs xs = zip xs (tail xs)
| pairSum (a, b) = a + b startingWithDigit d x = d ==
| head (show x)
|
| > [100960734,100960764,100960792,100960800,100960812]
|
| > 3 MiB total memory in use (0 MB lost due to fragmentation)
| agent327 wrote:
| More readable? How about this:
|
| SELECT DISTINCT authors FROM books WHERE page_count > 1000;
| matejn wrote:
| I like the SQL solutions people posted. But what about this one
| in Prolog? ?- setof(Author,
| Book^Pages^(book_author(Book, Author), book_pages(Book, Pages),
| Pages > 1000), Authors).
|
| Depending on the structure of the Prolog database, it could be
| shorter: ?- setof(Author, Pages^(book(_,
| Author, Pages), Pages > 1000), Authors).
| zesterer wrote:
| 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 wrote:
| > 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.
| alextingle wrote:
| If you like to actually read the code, then being able to
| search for a variable name really helps comprehension.
| Shadowing makes that harder, by introducing multiple distinct
| objects with the same name.
|
| Now you need something like an IDE to easily follow the
| lifetime of an object. Introducing a heavyweight dependency
| like that, as a prerequisite for simply following the code
| easily, is... a poor choice.
| begueradj wrote:
| 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.
| deepsun wrote:
| > 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.
| davidw wrote:
| 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.
| khaledh wrote:
| This, and the prefix nature of operators. That's the primary
| reason every time I try to give Lisp a chance, I get turned off
| by the maze of parens that I have to unravel in my head,
| especially for long, nested calls.
|
| For Lispers, good for them on knowing how to wire their brains
| to read this effortlessly. For the rest of us, there's a reason
| why Python's syntax is so easy to read for most people.
| skydhash wrote:
| 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)
| davidw wrote:
| 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.
| skydhash wrote:
| 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.
| seeinglogic wrote:
| Author here.
|
| Thank you for all the thoughtful comments and great stuff I
| didn't think of (also has been a hot minute since I wrote the
| article).
|
| I appreciate the discussion!
| superjan wrote:
| This is, by a large margin, the best article about code
| complexity I have read in a looong time. Thanks.
| farceSpherule wrote:
| I was an engineering manager back in the day (Java). People would
| get lost in the sauce with bracket placement, number of tabs,
| etc., etc. In order to avoid religious battles, I instituted a
| code formatter/beautifier. It would format all code upon commit.
|
| Problem solved.
|
| Although another conversation, people did not want to document
| their code. So I took the carrot / stick approach. I had to
| approve all commits and if code did not have javadoc, I did not
| approve the commit. If your commit was not on time, then that
| impacted your performance which, in turn, impacted your pay.
| People bitched at first but whatever. At this particular place,
| we were trying to get bought. Having documentation and other IP
| made us more valuable. It forced devs to put actual thought into
| how to manage their time.
| mwkaufma wrote:
| All of the leading code samples look _fine_. This whole article
| is the kind of superficial bikeshedding feedback one gets from
| code-reviewers who are just phoning it in and don't care about
| the substance of the problem or solution.
| jorams wrote:
| This is an interesting article, but also rather unsatisfying. It
| very quickly jumps to conclusions and goes right back to opinion.
| I agree with several of those opinions, but opinion was
| explicitly not the point of the article.
|
| > Prefer to not use language-specific operators or syntactic
| sugars, since additional constructs are a tax on the reader.
|
| I don't think this follows from the metric. If a function
| contains three distinct operators, a language-specific operator
| that replaces all three of them in one go would reduce the
| "effort" of function. It's highly scenario-specific.
|
| > Chaining together map/reduce/filter and other functional
| programming constructs (lambdas, iterators, comprehensions) may
| be concise, but long/multiple chains hurt readability
|
| I don't think this follows either. One effect of these constructs
| when used right is that they replace other operators and reduce
| the "volume". Again this can go both ways.
|
| > ...case in point, these code snippets aren't actually
| equivalent!
|
| That's a very language-specific diagnosis, and arguably points at
| hard-to-read _language design_ in JS. The snippet otherwise doesn
| 't look like JS, but I'm not aware of another language for which
| this would apply. Indeed it is also commonly known as a "null-
| safe operator", because most languages don't have separate "null"
| and "undefined".
|
| > variable shadowing is terrible
|
| > long liveness durations force the reader to do keep more
| possible variables and variables in their head.
|
| These can arguably be contradictory, and that is why I am a huge
| fan of variable shadowing _in some contexts_ : By shadowing a
| variable you remove the previous instance from scope, rather than
| keeping both available.
| ezoe wrote:
| Any nesting and indirection burden the mental fatigue on
| understanding the code: nesting conditional statements, macros,
| functions, classes while it's necessary to use or we will have so
| many duplicated codes which also burden the mental fatigue
| anyway.
|
| There was an extreme argument on a SNS recently that someone
| claimed that he prohibit nesting if in their work.
|
| Shorter-lived Variables argument doesn't always work. One of the
| most horrible code I read use very short-lived variables:
|
| val_2 = f(val), val_3 = g(val), ...
|
| It's Erlang. Because Erlang's apparent variable isn't a variable,
| but just a name bound to a term.
___________________________________________________________________
(page generated 2025-03-11 23:00 UTC)