[HN Gopher] Cognitive load is what matters
___________________________________________________________________
Cognitive load is what matters
Author : zdw
Score : 458 points
Date : 2024-12-22 22:18 UTC (3 days ago)
(HTM) web link (minds.md)
(TXT) w3m dump (minds.md)
| xnx wrote:
| This applies to user experience as well. I've seen designers
| focus on number of items or number of clicks when mental effort /
| cognitive load is what matters. Sometimes picking from a list of
| 50 links is easier. Sometimes answering 7 yes/no questions is
| easier.
| dvt wrote:
| > The companies where we were like "woah, these folks are smart
| as hell" for the most part failed
|
| Being clever, for the most part, almost never buys you anything.
| Building a cool product has nothing to do with being particularly
| smart, and scaling said product also rarely has much to do with
| being some kind of genius.
|
| There's this pervasive Silicon Valley throughline of the mythical
| "10x engineer," mostly repeated by incompetent CEO/PM-types which
| haven't written a line of code in their lives. In reality, having
| a solid mission, knowing who your customer is, finding that
| perfect product market fit, and building something people love is
| really what building stuff is all about.
|
| At the end of the day, all the bit-wrangling in the world is in
| service of that goal.
| Scene_Cast2 wrote:
| Depends on how you define smart. I worked at a place where
| income was directly tied to the quality of the ML models.
| Building what people love wouldn't have been the best strategy
| there.
| jsd1982 wrote:
| Only if your goal is to be an entrepreneur. Not everyone chases
| that goal nor considers success in that fashion.
| Swizec wrote:
| We can measure and quantify this cognitive load! I've been
| researching this for a book and have found some really cool
| research from ~10 years ago. It seems people stopped thinking
| about this around when microservices became popular (but they
| have the same problems just with http/grpc calls instead).
|
| There are two main ways to measure this:
|
| 1. Cyclomatic/mccabe complexity tells you how hard an individual
| module or execution flow is to understand. The more decision
| points and branches, the harder. Eventually you get to "virtually
| undebuggable"
|
| 2. Architectural complexity measures how visible different
| modules are to each other. The more dependencies, the worse
| things get to work with. We can empirically measure that
| codebases with unclear dependency structures lead to bugs and
| lower productivity.
|
| I wrote more here: https://swizec.com/blog/why-taming-
| architectural-complexity-...
|
| The answer seems to be vertical domain oriented modules with
| clear interfaces and braindead simple code. No hammer factory
| factories.
|
| PS: the big ball of mud is the world's most popular architecture
| because it works. Working software first, _then_ you can figure
| out the right structure.
| rTX5CMRXIfFG wrote:
| I've been interested in the same topic for a while now and the
| most difficult part, when explaining the concept to other
| programmers and defending against it in coding
| standards/reviews, is how to prove that cognitive load exists.
|
| Cyclomatic complexity seems one indicator, but architectural
| complexity needs to be clarified. I agree that how much modules
| expose to each other is one trait, but again, needs
| clarification. How do you intend to go about this?
|
| Been thinking about custom abstractions (ie those that you
| build yourself and which do not come from the standard
| libraries/frameworks) needed to understand code and simply
| counting them; the higher the number, the worse. But it seems
| that one needs to find something in cognitive psychology to
| back up the claim.
| Swizec wrote:
| > Cyclomatic complexity seems one indicator, but
| architectural complexity needs to be clarified. I agree that
| how much modules expose to each other is one trait, but
| again, needs clarification. How do you intend to go about
| this?
|
| Too much to summarize in a comment, I recommend reading the
| 3-blog series linked above. Architectural complexity is
| pretty well defined and we have an exact way to measure it.
|
| Unfortunately there's little industry tooling I've found to
| expose this number on the day-to-day. There's 1 unpopular
| paid app with an awful business model - I couldn't even
| figure out how to try it because they want you to talk to
| sales first /eyeroll.
|
| I have some prototype ideas rolling around my brain but been
| focusing on writing the book first. Early experiments look
| promising.
|
| There IS backing from cognitive research too - working
| memory. We struggle to keep track of more than ~7 independent
| items when working. The goal of abstraction (and this essay's
| cognitive load idea) is to keep the number of independently
| moving or impacted pieces under 7 while working. As soon as
| your changes could touch more stuff than fits in your brain,
| it becomes extremely challenging to work with and you get
| those whack-a-mole situations where every bug you fix causes
| 2 new bugs.
| ScotterC wrote:
| Looks like a solid post with solid learnings. Apologies for
| hijacking the thread but I'd really love to have a discussion on
| how these heuristics of software development change with the
| likes of Cursor/LLM cyborg coding in the mix.
|
| I've done an extensive amount of LLM assisted coding and our
| heuristics need to change. Synthesis of a design still needs to
| be low cognitive load - e.g. how data flows between multiple
| modules - because you need to be able to verify the actual system
| or that the LLM suggestion matches the intended mental model.
| However, striving for simplicity inside a method/function matters
| way less. It's relatively easy to verify that an LLM generated
| unit test is working as intended and the complexity of the code
| within the function doesn't matter if its scope is sufficiently
| narrow.
|
| IMO identifying the line between locations where "low cognitive
| load required" vs "low cognitive load is unnecessary" changes the
| game of software development and is not often discussed.
| codespin wrote:
| With LLM generated code (and any code really) the interface
| between components becomes much more important. It needs to be
| clearly defined so that it can be tested and avoid implicit
| features that could go away if it were re-generated.
|
| Only when you know for sure the problem can't be coming through
| from that component can you stop thinking about it and reduce
| the cognitive load.
| johnklos wrote:
| One way that I explain cognitive load to people unfamiliar with
| the term is to imagine crossing a lawn that has both autumn
| leaves and dog poop, and picture how much more mental energy one
| expends when trying to not step on dog poop.
| rowanG077 wrote:
| Cognitive load is precisely why I love feature rich languages.
| Once you have internalized a language the features it has fall
| away in terms of cognitive load for me. In the same way I don't
| think about how to ride a bike while I'm riding a bike.
|
| In most cases having a simpler language forces additional
| complexity into a program which does noticable add to cognitive
| load.
| Etheryte wrote:
| I think this works only up to the point where the language gets
| too large and starts creating extra cognitive load all by
| itself. For me, C++ is a good example of a language that has
| too many bells and whistles, if I have to stop what I'm doing
| to look up some weird syntax construct, then having all those
| extra features stops being useful.
| rowanG077 wrote:
| I don't think largeness is the problem. It's language design.
| C++ is just really badly designed. I'd be very happy with a
| very large language that takes a long time to get familiar,
| if all the features in the language are well designed. IMO
| the current developer landscape is all about "fast
| onboarding", but that is the totally wrong metric to optimize
| for. To me it's the difference between someone walking and an
| airplane. Sure it's very easy to just start walking, you
| ain't going to go anywhere fast. On the other hand an
| airplane takes orders of magnitude longer to get going but
| once it does you won't ever catch up to it by walking.
| Etheryte wrote:
| I think this is a good point. If you learn a language and
| it's useful, you usually use it for many, many years. So
| long as the daily driving experience is great, onboarding
| doesn't have to be that important of a metric.
| lisper wrote:
| OMG, so much this.
|
| One of the biggest sources of cognitive load is poor language
| design. There are so many examples that I can't even begin to
| list them all here, but in general, any time a compiler gives you
| an error _and tells you how to fix it_ that is a big red flag.
| For example, if the compiler can tell you that there needs to be
| a semicolon _right here_ , that means that there does not in fact
| _need_ to be a semicolon right there, but rather that this
| semicolon is redundant with information that is available
| elsewhere in the code, and the only reason it 's needed is
| because the language design demands it, not because of any actual
| necessity to precisely specify the behavior of the code.
|
| Another red flag is boilerplate. _By definition_ boilerplate is
| something that you have to type not because it 's required to
| specify the behavior of the code but simply because the language
| design demands it. Boilerplate is always unnecessary cognitive
| load, and it's one sign of a badly designed language. (Yes, I'm
| looking at you, Java.)
|
| I use Common Lisp for my coding whenever I can, and one of the
| reasons is that it, uniquely among languages, allows me to change
| the syntax and add new constructs so that the language meets the
| problem and not the other way around. This reduces cognitive load
| tremendously, and once you get used to it, writing code in any
| other language starts to feel like a slog. You become keenly
| aware of the fact that 90% of your mental effort is going not
| towards actually solving the problem at hand, but appeasing the
| compiler or conforming to some stupid syntax rule that exists for
| no reason other than that someone at some time in the dim and
| distant past thought it might be a good idea, and were almost
| certainly wrong.
| gleenn wrote:
| Totally agree. I think the biggest and most important things a
| language designer chooses is what to disallow. For instance,
| private/package/public etc is one small example of an imposed
| restriction which makes it easier to reason about changing a
| large project because if e.g. something is private then you
| know it's okay and probably easy to refactor. The self-imposed
| restrictions save you mental effort later. I also love lisps
| but am a Clojure fan. This is because in Clojure, 90+% of the
| code is static functions operating on immutable data. That
| makes it extremely easy to reason about in the large. Those two
| restrictions are big and impose a lot of structure, but man I
| can tear around the codebase with a machete because there are
| so many things that code /can't do/. Also, testing is
| boneheaded simple because everything is just parameters in to
| those static functions and assert on the values coming out. I
| don't have to do some arduous object construction with all
| these factories if I need to mock anything, I can use "with-
| redefs" to statically swap function definitions too, which is
| clean and very easy to reason about. Choosing the things you mr
| language disallows is one of the most important things you can
| do to reduce cognitive load.
| kvark wrote:
| I disagree with the first point. Say, the compiler figured out
| your missing semicolon. Doesn't mean it's easy for another
| human to clearly see it. The compiler can spend enormous
| compute to guess that, and that guess doesn't even have to be
| right! Ever been in a situation where following the compiler
| recommendation produces code that doesn't work or even build?
| We are optimizing syntax for humans here, so pointing out some
| redundancies is totally fine.
| lisper wrote:
| > Doesn't mean it's easy for another human to clearly see it.
|
| Why do you think that matters? If it's not needed, then it
| should never have been there in the first place. If it helps
| to make the program readable by humans then it can be shown
| as part of the _rendering_ of the program on a screen, but
| again, that should be part of the work the _computer_ does,
| not the human. Unnecessary cognitive load is still
| unnecessary cognitive load regardless of the goal in whose
| name it is imposed.
| Calavar wrote:
| In languages (both natural and machine languages) a certain
| amount of syntax redundancy is a feature. The point of
| syntax "boilerplate" is to turn typos into syntax errors.
| When you have a language without any redundant syntactical
| features, you run the risk that your typo is also valid
| syntax, just with different semantics than what you
| intended. IMHO, that's much worse than dealing with a
| missing semicolon error.
| nullstyle wrote:
| > then it can be shown as part of the rendering of the
| program on a screen
|
| I disagree with this, and can most easily express my
| disagreement by pointing out that people look at code with
| a diversity of programs: From simple text editors with few
| affordances to convey a programs meaning apart from the
| plain text like notepad and pico all the way up to the full
| IDEs that can do automatic refactoring and structured
| editing like the Jet Brains suite, Emacs+Paredit, or the
| clearly ever-superior Visual Interdev 6.
|
| If people view code through a diversity of programs, then
| code's on-disk form matters, IMO.
| dwattttt wrote:
| Aside from the other good points, this thread is about
| cognitive load. If a language lets you leave off lots of
| syntactic elements & let the compiler infer from context,
| that also forces anyone else reading it to also do the
| cognitive work to infer from context.
|
| The only overhead it increases is the mechanical effort to
| type the syntax by the code author; they already had to
| know the context to know there should be two statements,
| because they made them, so there's no increased "cognitive"
| load.
| lisper wrote:
| I guess I didn't make this clear. I'm not advocating for
| semicolons to be made _optional_. I 'm saying that they
| should not be included in the language syntax _at all_
| unless they are _necessary_ for some semantic purpose.
| And this goes for _any_ language element, not just
| semicolons.
|
| The vast majority of punctuation in programming languages
| is unnecessary. The vast majority of type declarations
| are unnecessary. All boilerplate is unnecessary. All
| these things are there mostly because of tradition, not
| because there is any technical justification for any of
| it.
| glitchc wrote:
| No, as python and other languages amply demonstrate, the
| semicolon is for the compiler, not the developer. If the
| compiler is sophisticated enough to figure out that a
| semicolon is needed, it has become _optional_. That 's the
| OP's point.
| taormina wrote:
| But the language spec for Python is what allows for this,
| not the compiler. \n is just the magic character now except
| now we also need a \ to make multiline expressions. It's
| all trade offs, compilers are not magic
| scotty79 wrote:
| Scala then. Semicolons are optional but you still can
| have them if you need them
| lblume wrote:
| The obvious example would have been JavaScript, but
| nobody wants to say something positive about
| JavaScript...
| scotty79 wrote:
| JavaScript has some specific and unique issues. Some
| silly choices (like auto inserting of semi-colons after
| empty return) and source code routinely, intentionally
| getting mangled by minification.
| sokoloff wrote:
| If it's in the language spec as required there and I'm
| using a compiler that claims to implement that language
| spec, I want the compiler to raise the error.
|
| _Additionally_ offering help on how to fix it is welcome,
| but silently accepting not-language-X code as if it were
| valid language-X code is not what I want in a language-X
| compiler.
| pwdisswordfishz wrote:
| Yes, semicolons are totally unnecessary. That's why nobody who
| works on JavaScript has ever regretted that automatic semicolon
| insertion was added to the language. It has never prevented the
| introduction of new syntaxes to the language (like discussed
| here: <https://github.com/twbs/bootstrap/issues/3057#issuecomme
| nt-5...>), nor motivated the addition of awkward grammatical
| contortions like [no LineTerminator here].
| christophilus wrote:
| There are plenty of languages that don't require semicolons
| and yet manage to avoid those issues: Clojure, Go, Odin...
| jmyeet wrote:
| Clojure delineates everything by explicitly putting
| statements in parentheses (like any LISP). That's basically
| the same thing.
|
| Go is an interesting example but it gets away with this by
| being far stricter with syntax IIRC (for the record, I'm a
| fan of Go's opinionated formatting).
| scotty79 wrote:
| Also Scala
| the__alchemist wrote:
| Great points. I strongly agree with your first point.
| Regrettably, I haven't used any language that solves this. (But
| believe it's possible, and you've demonstrated with one I
| haven't used).
|
| I'm stuck between two lesser evils, not having the ideal
| solution you found: 1: Rust: Commits the sin you say. 2:
| Python, Kotlin, C++ etc: Commits a worse sin: Prints lots of
| words.. (Varying degrees depending on which of these), where I
| may or may not be able to tell what's wrong, and if I can, I
| have to pick it out of a text well.
|
| Regarding boilerplate: This is one of the things I dislike most
| about rust. (As an example). I feel like
| prefixing`#[derive(Clone, Copy, PartialEq)]` on every (non-
| holding) enum is a flaw. Likewise, the way I use structs almost
| always results in prefixing each field with `pub`. (Other
| people use them in a different way, I believe, which doesn't
| require this)
| wesselbindt wrote:
| > the compiler can tell you that there needs to be a semicolon
| right here
|
| I can see that this is an annoyance, but does it really
| increase cognitive load? For me language design choices like
| allowing arbitrary arguments to functions (instead of having a
| written list of allowed arguments, I have to keep it in my
| head), or not having static types (instead of the compiler or
| my ide keeping track of types, I have to hold them in my head)
| are the main culprits for increasing cognitive load. Putting a
| semicolon where it belongs after the compiler telling me I have
| to is a fairly mindless exercise. The mental acrobatics I have
| to pull off to get anything done in dynamically typed languages
| is much more taxing to me.
| lisper wrote:
| Semicolons are just an example, and a fairly minor one. A
| bigger pet peeve of mine is C-style type declarations. If I
| create a binding for X and initialize it to 1, the compiler
| should be able to figure out that X is an integer without my
| having to tell it.
|
| In fact, all type declarations should be optional, with run-
| time dynamic typing as a fallback when type inferencing
| fails. Type "errors" should always be warnings. There should
| be no dichotomy between "statically typed" and "dynamically
| typed" languages. There should be a smooth transition between
| programs with little or no compile-time type information and
| programs with a lot of compile-time type information, and the
| compiler should do something reasonable in all cases.
| api wrote:
| I agree up until the end. Languages that let you change the
| syntax can result in stuff where every program is written in
| its own DSL. Ruby has this issue to some extent.
| lisper wrote:
| Sure, changing the syntax is not something to be done
| lightly. It has to be done judiciously and with great care.
| But it can be a huge win in some cases. For example, take a
| look at:
|
| https://flownet.com/gat/lisp/djbec.lisp
|
| It implements elliptic curve cryptography in Common Lisp
| using an embedded infix syntax.
| bloopernova wrote:
| Regarding Common Lisp, do you know of any articles that
| highlight the methods used to _" change the syntax and add new
| constructs so that the language meets the problem and not the
| other way around."_
| webnrrd2k wrote:
| It's talking about lisp macros, idempotent languages, and a
| few other features of lispey languages. I'd suggest the book
| On Lisp, or Lisp in Small Pieces as good places to learn
| about it, but there are a ton of other resources that may be
| better suited to your needs.
| webnrrd2k wrote:
| Also check out clojure, and the books: Norvig's PAIP, or
| Graham's ANSI Common Lisp.
| corinroyal wrote:
| And don't miss Sonja Keene's book "Object-Oriented
| Programming in Common Lisp" and Kiczales' "The Art of the
| Meta-Object Protocol". If you don't reach enlightenment
| after those, Libgen will refund your money.
| deergomoo wrote:
| > Another red flag is boilerplate. By definition boilerplate is
| something that you have to type not because it's required to
| specify the behavior of the code but simply because the
| language design demands it. Boilerplate is always unnecessary
| cognitive load, and it's one sign of a badly designed language.
| (Yes, I'm looking at you, Java.)
|
| The claim that LLMs are great for spitting out boilerplate has
| always sat wrong with me for this reason. They _are_ , but
| could we not spend some of that research money on eliminating
| some of the need for boilerplate, rather than just making it
| faster to input?
| JasonSage wrote:
| > Another red flag is boilerplate.
|
| I have to disagree. Boilerplate can simply be a one-time cost
| that is paid at setup time, when somebody is already required
| to have an understanding of what's happening. That boilerplate
| can be the platform for others to come along and easily
| read/modify something verbose without having to go context-
| switch or learn something.
|
| Arguing against boilerplate to an extreme is like arguing for
| DRY and total prevention of duplicated lines of code. It
| actually increases the cognitive load. Simple code to read and
| simple code to write is low-cost, and paying a one-time cost at
| setup is low compared to repeated cost during maintenance.
| singingfish wrote:
| I've had some C# code inflicted on me recently that follows
| the pile of garbage design pattern. Just some offshore guys
| fulfilling the poorly expressed spec with as little brain
| work as possible. The amount of almost-duplicate boilerplate
| kicking around is one of the problems. Yeah it looks like the
| language design encourages this lowest common denominator
| type approach, and has lead into the supplier providing code
| that needs substantial refactoring in order be able to create
| automated tests as the entry points ignore separation of
| concerns and abuse private v public members to give the
| pretense of best practices while in reality providing worst
| practice modify this code at your peril instead. It's very
| annoying because I could have used that budget to do
| something actually useful, but on the other hand improves my
| job security for now.
| JasonSage wrote:
| Sounds like you would have had problems whether there was
| boilerplate-y code or not.
| PittleyDunkin wrote:
| > Another red flag is boilerplate. By definition boilerplate is
| something that you have to type not because it's required to
| specify the behavior of the code but simply because the
| language design demands it.
|
| Two things: 1) this is often not language design but rather
| framework design, and 2) any semantic redundancy _in context_
| can be called boilerplate. Those same semantics may not be
| considered boilerplate in a different context.
|
| And on the (Common) Lisp perspective--reading and writing lisp
| is arguably a unique skill that takes time and money to develop
| and brings much less value in return. I'm not fan of java from
| an _essentialist_ perspective, but much of that cognitive load
| can be offset by IDEs, templates, lint tooling, etc etc. It has
| a role, particularly when you need to marshall a small army of
| coders very rapidly.
| frenchslumber wrote:
| Completely agree! Common Lisp is truly the tool of the Gods.
| nradov wrote:
| The criticisms of Java syntax are somewhat fair, but it's
| important to understand the historical context. It was first
| designed in 1995 and intended to be an easy transition for C++
| programmers (minimal cognitive load). In an alternate history
| where James Gosling and his colleagues designed Java "better"
| then it would have never been widely adopted and ended up as a
| mere curiosity like Common Lisp is today. Sometimes you have to
| meet your customers where they are.
|
| It has taken a few decades but the latest version significantly
| reduces the boilerplate.
| 3abiton wrote:
| My gripe with the post is that there is no objective "cognitive
| load" solution. Arguably this varies from 1 person to another.
| epolanski wrote:
| I don't think you can have golden rules, if you do, you fall
| in the usual don't do X, or limit Y to Z lines, etc.
|
| But what you _can_ do is to ask yourself whether you're
| adding or removing cognitive load as you work and seek
| feedback from (possibly junior) coworkers.
| jmyeet wrote:
| So with semi-colons, you have three basic options:
|
| 1. Not required (eg Python, Go)
|
| 2. Required (eg C/C++, Java)
|
| 3. Optional (eg Javascript)
|
| For me, (3) is by far the worst option. To me, the whole ASI
| debate is so ridiculous. To get away with (1), the languages
| make restrictions on syntax, most of which I think are
| acceptable. For example, Java/C/C++ allow you to put multiple
| statements on a single line. Do you need that? Probably not. I
| can't even think of an example where that's useful/helpful.
|
| "Boilerplate" becomes a matter of debate. It's a common
| criticism with Java, for example (eg anonymous classes). I
| personally think with modern IDEs it's really a very minor
| issue.
|
| But some languages make, say, the return statement optional. I
| actually don't like this. I like a return being explicit and
| clear in the code. Some will argue the return statement is
| boilerplate.
|
| Also, explicit type declarations can be viewed as boilerplate..
| There are levels to this. C++'s auto is one-level. So are "var"
| declarations. Java is more restrictive than this (eg <> for
| implied types to avoid repeating types in a single
| declaration). But is this boilerplate?
|
| Common Lisp is where you lose me. Like the meme goes, if CL was
| a good idea it would've caught on at some point in the last 60
| years. Redefning the language seems like a recipe for disaster,
| or at least adding a bunch of cognitive load because you can't
| trust that "standard" functions aren't doing standard things.
|
| Someone once said they like in Java that they're never
| surprised by 100 lines of code of Java. Unlike CL, there's
| never a parser or an interpreter hidden in there. Now that's a
| testament to CL's power for sure. But this kind of power just
| isn't conducive to maintainable code.
| epolanski wrote:
| I like (3) to be honest and the number of times it has poised
| any issue is virtually 0.
| cess11 wrote:
| You can reduce your Java boilerplate to annotations or succinct
| XML or whatever. Code generation is used a lot on the JVM.
|
| Can you show a real compiler message about such a semicolon?
| epolanski wrote:
| The very same reasons you find CL to lower your cognitive load
| are why ultimately after 60 years all lisps have been relegated
| to niche languages despite their benefits, and I say it as a
| Racket lover. It raises cognitive load for everybody else by
| having to go through further steps into decoding your choices.
|
| It's the very same reason why Haskell monocle-wielding
| developers haven't been able to produce one single killer
| software in decades: every single project/library out there has
| its own extensions to the language, specific compiler flags,
| etc that onboarding and sharing code becomes a huge chore. And
| again I say it as an avid Haskeller.
|
| Haskellers know that, and there was some short lived simple
| Haskell momentum but it died fast.
|
| But choosing Haskell or a lisp (maybe I can exclude Clojure
| somewhat) at work? No, no and no.
|
| Meanwhile bidonville PHP programmers can boast Laravel, Symfony
| and dozens of other libraries and frameworks that Haskellers
| will never ever be able to produce. Java?
|
| C? Even more.
|
| The language might be old and somewhat complex, but read a line
| and it means the same in any other project, there are no
| surprises only your intimacy with the language limiting you.
| There's no ambiguity.
| vonnik wrote:
| Totally agree with this and would add that cognitive load is not
| just a matter of the code before you, but a function of your
| total digital environment:
|
| https://vonnik.substack.com/p/how-to-take-your-brain-back
|
| Interruptions and distractions leave a cognitive residue that
| drastically reduces working memory through the Zeigarnik effect.
| lr4444lr wrote:
| _Mantras like "methods should be shorter than 15 lines of code"
| or "classes should be small" turned out to be somewhat wrong._
|
| So much this.
|
| The whole point of functions and classes was to make code
| _reusable_. If the entire contents of a 100 line method are only
| ever used in that method and it 's not recursive or using
| continuations or anything else weird, why the hell would it be
| "easier to read" if I had to jump up and down the file to 7
| different submethods when the function's entire flow is always
| sequential?
| Freak_NL wrote:
| For unit testing those sub-sections in a clear and concise
| manner (i.e., low cognitive load). As long as the method names
| are descriptive no jumping to and fro is needed usually.
|
| That doesn't mean every little unit needs to be split out, but
| it can make sense to do so if it helps write and debug those
| parts.
| oxidant wrote:
| Then you need to make those functions public, when the goal
| is to keep them private and unusable outside of the parent
| function.
|
| Sometimes it's easy to write multiple named functions, but
| I've found debugging functions can be more difficult when the
| interactions of the sub functions contribute to a bug.
|
| Why jump back and forth between sections of a module when I
| could've read the 10 lines in context together?
| Freak_NL wrote:
| > Then you need to make those functions public, [...]
|
| That depends on the language, but often there will be a way
| to expose them to unit tests while keeping them limited in
| exposure. Java has package private for this, with Rust the
| unit test sits in the same file and can access private
| function just fine. Other languages have comparable idioms.
| logicchains wrote:
| >why the hell would it be "easier to read" if I had to jump up
| and down the file to 7 different submethods when the function's
| entire flow is always sequential?
|
| If the submethods were clearly named then you'd only need to
| read the seven submethod names to understand what the function
| did, which is easier than reading 100 lines of code.
| fragmede wrote:
| Why is that any easier than having comments in the code that
| describe each part? In languages that don't allow closures,
| there's no good way to pass state between the seven functions
| unless you pass all the state you need, either by passing all
| the variables directly, or by creating an instance of a
| class/struct/whatever to hold those same variables and
| passing that. If you're lucky it might only be a couple of
| variables, but one can imagine that it could be a lot.
| spicyusername wrote:
| If all the functions need state from all the other
| functions, that is the problem a class or a struct solves -
| e.g. a place to store shared state.
|
| If the 7 things are directly related to one another and are
| _really_ not atomic things (e.g. "Find first user email",
| "Filter unknown hostnames", etc), then they can be in a big
| pile in their own place, but that is typically pretty rare.
|
| In general, you really want to let the code be crisp enough
| and your function names be intuitive enough that you don't
| need comments. If you have comments above little blocks of
| code like "Get user name and reorder list", that should
| probably just go into its own function.
|
| Typically I build my code in "layers" or "levels". The
| lowest level is a gigantic pile of utility functions. The
| top level is the highest level abstractions of whatever
| framework or interface I'm building. In the middle are all
| the abstractions I needed to build to bridge the two,
| typically programs are between 2-4 layers deep. Each layer
| should have all the same semantics of everything else at
| that layer, and lower layers should be less abstract than
| higher layers.
| fragmede wrote:
| My problem with the class/struct approach is it doesn't
| work if you don't need everything everywhere.
| foo(...): f1(a,b,c,d,e,f)
| f2(a,c,d,f) f3(b,c,d,e) ...
| f7(d,e)
|
| But with long descriptive variable names that you'd
| actually use so the function calls don't fit on one line.
| Better imo to have a big long function instead of a class
| and passing around extra variables.
|
| Though, ideally there isn't this problem in the first
| place/it's refactored away (if possible).
| psychoslave wrote:
| A function that needs so many parameters is already a no
| go.
|
| If it doesn't return anything, then it's either a method
| in a class, or it's a thing that perform some tricky side
| effect that will be better completely removed with a more
| sound design.
| edflsafoiewq wrote:
| It works fine. Not all the methods need to use all the
| struct members.
| hackinthebochs wrote:
| Language syntax defines functional boundaries. A strong
| functional boundary means you don't have to reason about
| how other code can _potentially_ influence your code, these
| boundaries are clearly defined and enforced by the
| compiler. If you just have one function with blocks of code
| with comments, you still must engage with the potential for
| non-obvious code interactions. That 's much higher
| cognitive load than managing the extra function with its
| defined parameters.
| fragmede wrote:
| In the ideal case, sure, but if assuming this can't be
| refactored, then the code foo(...):
| // init f1(a,b,c,d,e,f)
| f2(a,b,c,d,e,f) ... f7(a,b,c,d,e,f)
|
| or the same just with a,b,c,d,e,f stuffed into a
| class/struct and passed around, isn't any easier to
| reason about than if those functions are inline.
| joshuamorton wrote:
| There's at least one reason that something like this is
| going to be exceedingly rare in practice, which is that
| (usually) functions return things.
|
| In certain cases in C++ or C you might use in/out params,
| but those are less necessary these days, and in most
| other languages you can just return stuff from your
| functions.
|
| So in _almost_ every case, f1 will have computed some
| intermediate value useful to f2, and so on and so forth.
| And these intermediate values will be arguments to the
| later functions. I 've basically never encountered a
| situation where I _can 't_ do that.
|
| Edit: and as psychoslave mentions, the arguments
| themselves can be hidden with fluent syntax or by
| abstracting a-f out to a struct and a fluent api or
| `self`/`this` reference.
|
| Cases where you only use some of the parameters in each
| sub-function are the most challenging to cleanly
| abstract, but are also the most useful because they help
| to make complex spaghetti control-flow easier to follow.
| psychoslave wrote:
| This typically can be coded with something like
|
| def foo(...) = Something.new(...).f1.f2.f7
|
| Note that ellipsis here are actual syntax in something
| like Ruby, other languages might not be as terse and
| convinient, but the fluent pattern can be implemented
| basically everywhere (ok maybe not cobol)
| thfuran wrote:
| >Why is that any easier than having comments in the code
| that describe each part?
|
| Because 7<<100
| TeMPOraL wrote:
| > _Because 7 <<100_
|
| But then, 7 << 100 << (7 but each access blanks out your
| short-term memory), which is how jumping to all those
| tiny functions and back plays out in practice.
| joshuamorton wrote:
| Why does pressing "go to defn" blank your short term
| memory in a way that code scrolling beyond the top of the
| screen doesn't?
| lazyasciiart wrote:
| > there's no good way to pass state between the seven
| functions unless you pass all the state you need,
|
| That's why it's better than comments: because it gives you
| clarity on what part of the state each function reads or
| writes. If you have a big complex state and a 100 line
| operation that is entirely "set attribute c to d, set
| attribute x to off" then no, you don't need to extract
| functions, but it's possible that e.g this method belongs
| inside the state object.
| lr4444lr wrote:
| If the variables were clearly named, I wouldn't have to read
| much at all, unless I was interested in the details. I
| reitrate: why does the _length_ of the single function with
| no reuse matter?
| joshuamorton wrote:
| I find "is_enabled(x)" to be easier to reason about than
| if (x.foo || x.bar.baz || (x.quux && x.bar.foo))
|
| Even if it's only ever used once. Functions and methods provide
| abstraction which is useful for more than just removing
| repetition.
| gizzlon wrote:
| Wouldn't you jump to is_enabled to see what it does?
|
| That's what I always do in new code, and probably why I
| dislike functions that are only used once or twice. The
| overhead of the jump is not worth it. is_enabled could be a
| comment above the block (up to a point, notif it's too long)
| joshuamorton wrote:
| > Wouldn't you jump to is_enabled to see what it does?
|
| That depends on a lot of things. But the answer is
| (usually) no. I might do it if I think the error is
| specifically in that section of code. But especially if you
| want to provide any kind of documentation or history on why
| that code is the way it is, it's easier to abstract that
| away into the function.
|
| Furthermore, most of the time code is being read isn't the
| first time, and I emphatically don't want to reread some
| visual noise every time I am looking at a larger piece of
| code.
| emn13 wrote:
| If you're literally using it just once, why not stick it in a
| local variable instead? You're still getting the advantage of
| naming the concept that it represents, without eroding code
| locality.
|
| However, the example is a slightly tricky basis to form an
| opinion on best practice: you're proposing that the clearly
| named example function name is_enabled is better than an
| expression based on symbols with gibberish names. Had those
| names (x, foo, bar, baz, etc) instead been well chosen
| meaningful names, then perhaps the inline expression would
| have been just as clear, especially if the body of the if
| makes it obvious what's being checked here.
|
| It all sounds great to introduce well named functions in
| isolated examples, but examples like that are intrinsically
| so small that the costs of extra indirection are irrelevant.
| Furthermore, in these hypothetical examples, we're kind of
| assuming that there _is_ a clearly correct and unique
| definition for is_enabled, but in reality, many ifs like this
| have more nuance. The if may well not represent if-enabled,
| it might be more something like was-enabled-last-app-startup-
| assuming-authorization-already-checked-unless-io-error. And
| the danger of leaving out implicit context like that is
| precisely that it sounds simple, is_enabled, but that
| simplicity hides corner cases and unchecked assumptions that
| may be invalidated by later code evolution - especially if
| the person changing the code is _not_ changing is_enabled and
| therefore at risk of assuming it really means whether
| something is enabled regardless of context.
|
| A poor abstraction is worse than no abstraction. We need
| abstractions, but there's a risk of doing so recklessly. It's
| possible to abstract too little, especially if that's a sign
| of just not thinking enough about semantics, but also to
| abstract too much, especially if that's a sign of thinking
| superficially, e.g. to reduce syntactic duplication
| regardless of meaning.
| lazyasciiart wrote:
| Pretty sure every compiler can manage optimizing out that
| method call, so do whichever makes you and your code
| reviewer happy.
| joshuamorton wrote:
| A local variable is often _worse_ : Now I suffer both the
| noise of the unabstracted thing, and an extra assignment.
| While part of the goal is to give a reasonable logical name
| to the complex business logic, the other value is to hide
| the business logic for readers who truly don't care (which
| is most of them).
|
| The names could be better and more expressive, sure, but
| they could also be function calls themselves or long and
| difficult to read names, as an example:
| if ( x.is_enabled ||
| x.new_is_enabled || (x.in_us_timezone &&
| is_daytime()) ||
| x.experimental_feature_mode_for_testing )...
|
| That's somewhat realistic for cases where the abstraction
| is covering for business logic. Now if you're lucky you can
| abstract that away entirely to something like an injected
| feature or binary flag (but then you're actually doing what
| I'm suggesting, just with extra ceremony), but sometimes
| you can't for various reasons, and the same concept
| applies.
|
| In fact I'd actually strongly disagree with you and say
| that doing what I'm suggesting is even more important if
| the example is larger and more complicated. That's not an
| excuse to not have tests or not maintain your code well,
| but if your argument is functionally "we cannot write
| abstractions because I can't trust that functions do what
| they say they do", that's not a problem with abstractions,
| that's a problem with the codebase.
|
| I'm arguing that keeping the complexity of any given stanza
| of code low is important to long-term maintainability, and
| I think this is true because it invites a bunch of really
| good questions and naturally pushes back on some increases
| in complexity: if `is_enabled(x)` is the current state of
| things, there's a natural question asked, and inherent
| pushback to changing that to `is_enabled(x, y)`. That's
| _good_. Whereas its much easier for natural development of
| the god-function to result in 17 local variables with
| complex interrelations that are difficult to parse out and
| track.
|
| My experience says that identifying, removing, and naming
| assumptions is vastly easier when any given function is
| small and tightly scoped and the abstractions you use to do
| so also naturally discourage other folks who develop on the
| same codebase from adding unnecessary complexity.
|
| And I'll reiterate: my goal, at least, when dealing with
| abstraction isn't to focus on duplication, but on clarity.
| It's worthwhile to introduce an abstraction even for code
| used once if it improves clarity. It may not be worthwhile
| to introduce an abstraction for something used many times
| if those things aren't inherently related. That creates
| unnecessary coupling that you either undo or hack around
| later.
| shivawu wrote:
| I agree except I think 100 lines is definitely worth a method,
| whereas 15 lines is obviously not worthy for the most cases and
| yet we do that a lot.
|
| My principle has always been: "is this part a isolated and
| intuitive subroutine that I can clearly name and when other
| people see it they'll get it at first glance without pausing to
| think what this does (not to mention reading through the
| implemention)". I'm surprised this has not been a common wisdom
| from many others.
| toasterlovin wrote:
| Yeah, I find extracting code into methods very useful for
| naming things that are 1) a digression from the core logic,
| and 2) enough code to make the core logic harder to
| comprehend. It's basically like, "here's this thing, you can
| dig into it if you want, but you don't have to." Or, the core
| logic is the top level summary and the methods it calls out
| to are sections or footnotes.
| andrewingram wrote:
| In recent years my general principle has been to introduce an
| abstraction (in this case split up a function) if it lowers
| local concepts to ~4 (presumably based on similar principles
| to the original post). I've taken to saying something along
| the lines of "abstractions motivated by reducing repetition
| or lines of code are often bad, whilst ones motivated by
| reducing cognitive load tend to be better".
|
| Good abstractions often reduce LOC, but I prefer to think of
| that as a happy byproduct rather than the goal.
| Mawr wrote:
| Because a function clearly defines the scope of the state
| within it, whereas a section of code within a long function
| does not. Therefore a function can be reasoned about in
| isolation, which lowers cognitive load.
| lr4444lr wrote:
| You can write long functions in a bad way, don't get me
| wrong. I'm just saying the rule that the _length itself_ is
| an anti-pattern has no inherent validity.
| sureglymop wrote:
| As a non English speaker, what does "so much this" mean?
|
| Does it essentially just mean "I agree"?
| scott_w wrote:
| Yep, basically "I agree with this statement a lot." It's very
| much an "online Americanism."
| ericjmorey wrote:
| It's a call for others to take note of the important or
| profound message being highlighted. So more than just "I
| agree".
| lr4444lr wrote:
| In the superlative, yes. It's a fairly new phrase, and hardly
| in my parlance, but it's growing on me when I'm in informal
| typed chat contexts.
| onionisafruit wrote:
| To paraphrase a recentish comment from jerf, "sometimes you
| just have a long list of tasks to do". That stuck with me. Now
| I'm a bit quicker to realize when I'm in that situation and
| don't bother trying to find a natural place to break up the
| function.
| runevault wrote:
| For me it depends. Sometimes I find value in making a
| function for a block of work I can give its own name to,
| because that can make the flow more obvious when looking at
| what the function does at a high level. But arbitrarily
| breaking up a function just because is silly and pointless.
| lostdog wrote:
| Plus, laying the list of tasks out in order sometimes makes
| it obvious how to split it up eventually. If you try to
| split it up the first time you write it, you get a bunch of
| meaningless splits, but if you write a 300 line function,
| and let it simmer for a few weeks, usually you can spot
| commonalities later.
| runevault wrote:
| That's also true, though in this case I'm not necessarily
| worried about commonalities, just changing the way it
| reads to focus on the higher level ideas making up the
| large function.
|
| But revisiting code after a time, either just because you
| slept on it or you've written more adjacent code, is
| almost always worth some time to try and improve the
| readability of the code (so long as you don't sacrifice
| performance unnecessarily).
| westcoast49 wrote:
| It comes down to the quality of the abstractions. If they are
| well made and well named, you'd rather read this:
| axios.get('https://api.example.com', { headers: {
| 'Authorization': 'Bearer token' }, params: { key:
| 'value' } }) .then(response =>
| console.log(response.data)) .catch(error =>
| console.error(error));
|
| than to read the entire implementations of get(), then() and
| catch() inlined.
| lll-o-lll wrote:
| > The whole point of functions and classes was to make code
| reusable.
|
| I'm amazed that here we are >40 years on from C++, and still
| this argument is made. Classes never encapsulated a module of
| reusability, except in toy or academic examples. To try and use
| them in this way either leads to gigantic "god" classes, or so
| many tiny classes with scaffolding classes between them that
| the "communication overhead" dwarfs the actual business logic.
|
| Code base after code base proves this again and again. I have
| never seen a "class" be useful as a component of re-use. So
| what is? Libraries. A public interface/api wrapping a "I don't
| care what you did inside". Bunch of classes, one class,
| methods? So long as the interface is small and well defined,
| who cares how it's structured inside.
|
| Modular programming can be done in any paradigm, just think
| about the api and the internal as separate things. Build some
| tests at the interface layer, and you've got documentation for
| free too! Re-use happens at the dll or cluster of dll
| boundaries. Software has a _physical_ aspect to it as well as
| code.
| Freak_NL wrote:
| That short/long toggle in the top-right seems to expand and
| collapse the article. It defaults to short. Reading this article
| in its short form I kept wondering if I was missing something
| relevant (cognitive load++), but with the long form on I kept
| wondering if some paragraphs were explicitly intended to be
| superfluous or tangential (cognitive load++) for the sake of that
| collapsing trick.
|
| For an article on cognitive load, using a gimmick which increases
| it seems ironic.
| dangoodmanUT wrote:
| I thought these things were pretty inferable
| BiteCode_dev wrote:
| Something I noticed is that some vim / keyboard only envs are
| paying a huge cognitive load price by holding various states in
| their mind and having to expand efforts every time they switching
| context.
|
| Sometimes there is the added burden of an exotic linux distro or
| a dvorak layout on a specially shaped keyboard.
|
| Now, some devs are capable of handling this. But not all do, I've
| seen many claiming they are more productive with it, but when
| compared to others, they were less productive.
|
| They were slow and tired easily. They had a higher burn out rate.
| The had too much to pay upfront for their day to day coding task
| but couldn't see that their idealization of their situation was
| not matching reality.
|
| My message here is: if you are in such env be very honest with
| yourself. Are you good enough that you are among the few that
| actually benefit from it?
| guywhocodes wrote:
| Is this bait?
| cainxinth wrote:
| I think it's all about the framework, the memory palace you build
| to keep things organized. A secondary factor is the freedom and
| solitude to prevent extraneous concerns from interrupting you.
| The brain is not great at true multitasking (doing two or more
| things at the same time), but it can juggle.
| hn8726 wrote:
| I agree with vast majority of the post, and it matches my
| experience. What I'm not sure I follow is the part about layered
| architecture, and what is offered as an alternative. The author
| quickly gets to a _conclusion_ that
|
| > So, why pay the price of high cognitive load for such a layered
| architecture, if it doesn't pay off in the future?
|
| where one of the examples is
|
| > If you think that such layering will allow you to quickly
| replace a database or other dependencies, you're mistaken.
| Changing the storage causes lots of problems, and believe us,
| having some abstractions for the data access layer is the least
| of your worries.
|
| but in my experience, it's crucial to abstract away -- even if
| the interface is not ideal -- external dependencies. The point is
| not to be able to "replace a database", but to _own_ the
| interface that is used by the application. Maybe the author only
| means _unnecessary layering_, but the way the argument is framed
| seems like using external dependency APIs throughout the entire
| app is somehow better.
| master_crab wrote:
| I commented on this in another thread here.
|
| What I read it as is don't over-index on creating separate
| layers/services if they are already highly dependent on each
| other. It just adds additional complexity tracing dependencies
| over the networking stack, databases/datastores, etc that the
| services are now split across.
|
| In other words: a monolithic design is acceptable if the
| services are highly intertwined and dependent.
| mrkeen wrote:
| I think the 'Layered Architecture' section is all over the
| place.
|
| There are a lot of terms thrown around with pretty loose
| definitions - in this article and others. I had to look up
| "layered architecture" to see what other people wrote about it,
| and it looks like an anti-pattern to me: In a
| four-layered architecture, the layers are typically divided
| into: Presentation Application Domain
| Infrastructure These layers are arranged in a
| hierarchical order, where each layer provides services to the
| layer above it and uses services from the layer below it, and
| each layer is responsible for handling specific tasks and has
| limited communication with the other layers. [1]
|
| It looks like an anti-pattern to be because, as described, each
| layer _depends_ on the one below it. It looks like how you 'd
| define the "dependency non-inversion" principle. _Domain_
| depends on _Infrastructure_? A BankBalance is going to depend
| on MySQL? Even if you put the DB behind an interface, the
| direction of dependencies is still wrong: BankBalace-
| >IDatabase.
|
| Back to TFA:
|
| > In the end, we gave it all up in favour of the good old
| dependency inversion principle.
|
| OK. DIP is terrific.
|
| > No port/adapter terms to learn
|
| There is a big overlap between ports/adapters, hexagonal, and
| DIP: Allow an application to equally be driven
| by users, programs, automated test or batch scripts, and to be
| developed and tested in isolation from its eventual run-time
| devices and databases. [2]
|
| That is, the Domain ("application") is at the bottom of the
| dependency graph, so that the Infrastructure {Programs, Tests,
| Scripts} can depend upon it.
|
| > If you think that such layering will allow you to quickly
| replace a database or other dependencies, you're mistaken.
|
| _Layering_ will not help - it will hinder, as I described
| above. But you should be able to quickly replace any dependency
| you like, which is what DIP /PortsAdapters/Hexagonal gives you.
|
| > Changing the storage causes lots of problems, and believe us,
| having some abstractions for the data access layer is the least
| of your worries. At best, abstractions can save somewhat 10% of
| your migration time (if any)
|
| I iterate on my application code without spinning up a
| particular database. Same with my unit tests. Well worth it.
|
| [1] https://bitloops.com/docs/bitloops-
| language/learning/softwar... [2]
| https://alistair.cockburn.us/hexagonal-architecture/
| d0mine wrote:
| What causes more cognitive load: filter(odd,
| numbers)
|
| vs. (n for n in numbers if odd(n))
|
| It depends on the reader too.
| SoftTalker wrote:
| Also depends on whether it's obvious why I need a list of odd
| numbers.
| fferen wrote:
| Disagree with first example. If that condition is only used once,
| adding a variable introduces more state to keep track of, that
| could just be a comment next to the conditional.
| emptiestplace wrote:
| The "too smart developers" narrative is pandering - poor design
| stems from inexperience and its accompanying insecurity, not
| intelligence. Skilled developers intuitively grasp the value of
| simplicity.
| fallingknife wrote:
| I find that it comes most from intelligence. I see plenty of
| super experienced but not very smart engineers design terrible
| over engineered systems. On the other hand, juniors err in the
| opposite direction with long functions with deep nested
| branching and repetition. And the latter is better. Easier to
| refactor up in abstraction level than down.
| emptiestplace wrote:
| Perhaps I'm confused, but it seems to me that your examples
| actually support my point. You're describing experience-based
| patterns - seniors over-abstracting vs juniors writing
| tangled code. Neither case is about intelligence; they're
| about different types of inexperience leading to different
| design mistakes.
| nostradumbasp wrote:
| I love what you're saying. But, I've met a lot of people who
| have say 10-20 years experience designing applications with
| unnecessary and sometimes incredible cognitive load. There are
| serious incentives to NOT write "simple" code, let me share a
| few of them.
|
| Root causes from my perspective look like: 1. Job security type
| development. Fearful/insecure developers make serious puzzle
| boxes. "Oh yea wait until they fire me and see how much they
| need me, I'm the only one who can do this."
|
| 2. Working in a vacuum/black hole developers. Red flags are
| phrases like " _snark_ I could have done this " when working
| together on a feature with them. Yes, that is exactly the
| point, and I even hope the junior comes in after and can build
| off of it too.
|
| 3. Mixing work with play "I read this blog post about category
| theory and found this great way to conceptualize my code
| through various abstractions that actually deter from runtime
| performance but sound really cool when we talk about it at
| lunch".
|
| 4. Clout/resume/ego chasing "I want to say something smart at
| stand up, a conference, or at a future job, so other people
| know they are not on my level and cannot touch my code or
| achieve my quality."
|
| Some other red flags. They alone maintain their "pet" projects
| for everything serious until they couldn't. Minor
| problems/changes come up, someone else goes in and fixes it.
| Something serious happens it's a stop the world garbage
| collection for that developer and they are the only one who can
| fix it disrupting any other operations they were part of.
| marginalia_nu wrote:
| Yeah it's kind of a weird narrative. Writing complex code is
| leaps and bounds easier than writing simple code. Often takes
| both experience and intelligence to see the correct way.
| deergomoo wrote:
| Composition over inheritance is one of the most valuable lessons
| I learned earlier in my career as a developer. In fact these
| days, I'm hard-pressed to think of a case in which I would prefer
| inheritance as my first choice to model _any_ problem. I 'm sure
| there probably are some, but it feels too easy to wield
| irresponsibly and let bad design creep in.
|
| At a previous job I had, a fairly important bit of code made use
| of a number of class hierarchies each five or six layers deep,
| including the massive code smell/design failure of certain layers
| stubbing out methods on a parent class due to irrelevancy.
|
| To make matters worse, at the point of use often only the
| base/abstract types were referenced, so even working out what
| code was running basically required stepping through in a
| debugger if you didn't want to end up like the meme of Charlie
| from Always Sunny. And of course, testing was a nightmare because
| everything happened internally to the classes, so you would end
| up extending them even further in tests just to stub/mock bits
| you needed to control.
| awinter-py wrote:
| > AdminController extends UserController extends GuestController
| extends BaseController
|
| > Cognitive load in familiar projects -- If you've internalized
| the mental models of the project into your long-term memory, you
| won't experience a high cognitive load.
|
| ^ imo using third-party libraries checks both of these boxes
| because 1) a fresh-to-project developer with general experience
| may already know the 3rd party lib, and 2) third party libraries
| compete in the ecosystem and the easiest ones win
| fmxsh wrote:
| > "Having too many shallow modules can make it difficult to
| understand the project. Not only do we have to keep in mind each
| module responsibilities, but also all their interactions."
|
| Not only does it externalize internal complexity, but it creates
| emergent complexity beyond what would arise between and within
| deeper modules.
|
| In a sense, shallow modules seem to be like spreading out the
| functions outside the conceptual class while thinking the
| syntactical encapsulation itself, rather than the content of the
| encapsulation, is the crucial factor.
| master_crab wrote:
| On the layered architecture section:
|
| I have seen too many architectures where an engineer took
| "microservices" too far and broke apart services that almost
| always rely on each other into separate containers/VMs/serverless
| functions.
|
| I'm not suggesting people build monolithic applications, but it's
| not necessarily a good idea to break every service into its own
| distinct stack.
| Aurornis wrote:
| > Mantras like "methods should be shorter than 15 lines of code"
| or "classes should be small" turned out to be somewhat wrong.
|
| These hard rules may be useful when trying to instill good habits
| in juniors, but they become counterproductive when you start
| constraining experienced developers with arbitrary limits.
|
| It's really bad when you join a team that enforces rules like
| this. It almost always comes from a lead or manager who reads too
| many business books and then cargo cults those books on to the
| team.
| deergomoo wrote:
| Same deal with DRY, the principle is obviously correct but
| people can take it too literally. It's so easy to get yourself
| in a huge mess trying to extract out two or three bits of code
| that _look_ pretty similar but aren 't really used in the same
| context.
| skeeter2020 wrote:
| The problem with DRY and generic rules around size, etc.
| really seems to be figuring out the boundaries, and that's
| tough to get right, even for experienced devs, plus very
| contextual. If you need to open up a dozen files to make a
| small change you're overwhelmed, but then if you need to wade
| through a big function or change code in 2 places you're just
| as frustrated.
| quesomaster9000 wrote:
| This is the bane of my existence at the moment after ~20 years
| into my career, and it frustrates me when I run into these
| situations when trying to get certain people to review pull
| requests (because I'm being kind, and adhering to a process,
| and there is really valuable feedback at times). But on the
| whole it's like being dragged back down to working at a snails
| pace.
|
| - Can't refactor code because it changes too many files and too
| many lines.
|
| - Can't commit large chunks of well tested code that 'Does
| feature X', because... too many files and too many lines.
|
| - Have to split everything down into a long sequence of
| consecutive pull requests that become a process nightmare in
| its own right
|
| - The documentation comments gets nitpicked to death with
| mostly useless comments about not having periods at the ends of
| lines
|
| - End up having to explain every little detail throughout the
| function as if I'm trying to produce a lecture, things like `/*
| loop until not valid */ while (!valid) {...` seemed to be what
| they wanted, but to me it made no sense what so ever to even
| have that comment
|
| This can turn a ~50 line function into a 3 day process, a
| couple of hundred lines into a multi-week process, and a
| thousand or two line refactor (while retaining full test
| coverage) into a multi-month process.
|
| At one point I just downed tools and quit the company, the
| absurdity of it all completely drained my motivation, killed
| progress & flow and lead to features not being shipped.
|
| Meanwhile with projects I'm managing I have a fairly good
| handle on 'ok this code isnt the best, but it does work, it is
| fairly well tested, and it will be shipped as the beta', so as
| to not be obstinate.
| NotBoolean wrote:
| I don't have your experience but I personally think some of
| this feedback can be warranted.
|
| > Can't refactor code because it changes too many files and
| too many lines.
|
| This really depends on the change. If you are just doing a
| mass rename like updating a function signature, fair enough
| but if you changing a lot of code it's very hard to review
| it. Lots of cognitive load on the reviewer who might not have
| the same understanding of codebase as you.
|
| > Can't commit large chunks of well tested code that 'Does
| feature X', because... too many files and too many lines.
|
| Same as the above, reviewing is hard and more code means
| people get lazy and bored. Just because the code is tested
| doesn't mean it's correct, just means it passes tests.
|
| > Have to split everything down into a long sequence of
| consecutive pull requests that become a process nightmare in
| its own right
|
| This is planning issue, if you correctly size tickets you
| aren't going to end up in messy situations as often.
|
| > The documentation comments gets nitpicked to death with
| mostly useless comments about not having periods at the ends
| of lines
|
| Having correctly written documentation is important. It can
| live a long time and if you don't keep an eye on it can
| becomes a mess. Ideally you should review it before you
| submitting it to avoid these issues.
|
| > End up having to explain every little detail throughout the
| function as if I'm trying to produce a lecture, things like
| `/* loop until not valid */ while (!valid) {...` seemed to be
| what they wanted, but to me it made no sense what so ever to
| even have that comment
|
| I definitely agree with this one. Superfluous comments are a
| waste of time.
|
| Obviously this is just my option and you can take things too
| far but I do think that making code reviewable (by making it
| small) goes a long way. No one wants to review 1000s lines of
| code at once. It's too much to process and people will do a
| worse job.
|
| Happy to hear your thoughts.
| quesomaster9000 wrote:
| I do object to the notion of something being a planning
| issue when you're talking about a days worth of work.
|
| Implement X, needs Y and Z, ok that was straightforward,
| also discovered U and V on the way and sorted that out,
| here's a pull request that neatly wraps it up.
|
| Which subsequently gets turned into a multi-week process,
| going back & forth almost every day, meaning I can't move
| on to the next thing, meanwhile I'm looking at the
| cumulative hourly wages of everybody involved and the cost
| is... shocking.
|
| Death by process IHMO.
| bspammer wrote:
| > Implement X, needs Y and Z, ok that was
| straightforward, also discovered U and V on the way and
| sorted that out, here's a pull request that neatly wraps
| it up
|
| This sounds very difficult to review to be honest. At a
| minimum unrelated changes should be in their own pull
| request (U and V in your example).
| pbh101 wrote:
| Agree. Another item here that is contextual: what is the
| cost of a bug? Does it cost millions, do we find that out
| immediately, or does it take months? Or does it not
| really matter, and when we'll find the big it will be
| cheap? The OP joining a new company might not have the
| context that existing employees have about why we're
| being cautious/clear about what we're changing as opposed
| to smuggling in refactors in the same PR as a feature
| change.
|
| I'm going to be the guy that is asking for a refactor to
| be in a separate commit/PR from the feature and clearly
| marked.
|
| It doesn't justify everything else he mentioned
| (especially the comments piece) but once you get used to
| this it doesn't need to extend timelines.
| lazyasciiart wrote:
| > This is planning issue, if you correctly size tickets you
| aren't going to end up in messy situations as often.
|
| No, it's "this refactor looks very different to the
| original code because the original code thought it was
| doing two different things and it's only by stepping
| through it with real customer data that you realized with
| the right inputs (not documented) it could do a third thing
| (not documented) that had very important "side effects" and
| was a no-op in the original code flow. Yea, it touches a
| lot of files. Ok, yea, I can break it up step by step, and
| wait a few days between approval for each of them so that
| you never have to actually understand what just happened".
| grey-area wrote:
| The way I normally approach this is one big pr for
| context and then break it into lots of small ones for
| review.
| withinboredom wrote:
| so, it's not just a refactoring then; it's also bug fixes
| + refactoring. In my experience, those are the worst PRs
| to review. Either just fix the bugs, or just refactor it.
| Don't do both because now I have to spend more time
| checking the bugs you claim to fix AND your refactoring
| for new bugs.
| callc wrote:
| > This is planning issue, if you correctly size tickets you
| aren't going to end up in messy situations as often.
|
| I think the underlying issue is what is an appropriate
| "unit of work". Parent commenter may want to ship a
| complete/entire feature in one MR. Ticketing obsessed
| people will have some other metric. Merge process may be
| broken in this aspect. I would rather explain to reviewer
| to bring them up to speed on the changes to make their
| cognitive load easier
| gjadi wrote:
| This. The solution to long and multiple reviews to MR is
| single pair review session where most of the big picture
| aspects can be addressed immediately and verbally
| discussed and challenged.
|
| IMHO it is the same as chat. If talking about an issue
| over mail or chat takes more than 3-5 messages, trigger a
| call to solve it face to face.
| nosefurhairdo wrote:
| That's rough. Of course some amount of thoughtfulness towards
| "smallest reasonable change" is valuable, but if you're not
| shipping then something is wrong.
|
| As for the "comments on every detail" thing... I would fight
| that until I win or have to leave. What a completely asinine
| practice to leave comments on typical lines of code.
| SeptiumMMX wrote:
| You always need to look at the track record of the team. If
| they were not producing solid consistent results before you
| joined them, it's a very good indicator that something's
| fishy. All that "they are working on something else that we
| can't tell you" is BS.
|
| If they were, and you were the only one treated like that,
| hiring you was a decision forced upon the team, so they got
| rid of you in a rather efficient way.
| lifeisstillgood wrote:
| I am trying my best to build in an _inordinate_ amount of
| upfront linting and automated checks just to avoid such
| things - and then I still need to do a roadshow, or lots of
| explanations- but that's probably good.
|
| But the good idea is to say "we all have the same brutal
| linting standards (including full stops in docs!) - so
| hopefully the human linger will actually start reading the
| code for what it is, not what it says"
| whstl wrote:
| I'm also a fan of linting everything. Custom linter rules
| ftw.
|
| This and documenting non-lintable standards so that people
| are on the same page ("we do controllers like this").
|
| This is how I like to build and run my teams. This makes
| juniors so much more confident because they can ship stuff
| from the get go without going through a lengthy nitpicky
| brutal review process. And more senior devs need to
| actually look at code and business rules rather than
| nitpicking silly shit.
| shinycode wrote:
| No wonder why software development used to be expensive if 50
| lines of code takes multiples days for several people ...
| LtWorf wrote:
| Well maybe they do critical systems.
| spion wrote:
| Indeed, cognitive load is not the only thing that matters.
| Non-cognitive toil is also a problem and often enough it
| doesn't get sufficient attention even when things get really
| bad.
|
| We do need better code review tools though. We also need to
| approach that process as a mechanism of effectively building
| good shared understanding about the (new) code, not just
| "code review".
| spockz wrote:
| This sounds more like a case where you need a "break-the-
| glass" like procedure where some checks don't apply. Or the
| checks should be non blocking anyway.
| notShabu wrote:
| there is huge incentive for people who don't know how to
| code/create/do-stuff to slow things down like this b/c it
| allows them many years of runway at the company.
|
| they are almost always cloaked in virtue signals.
|
| almost every established company you join will already have
| had this process going for a long time.
|
| doing stuff successfully at such a company is dangerous to
| the hierarchy and incurs an immune response to shut down or
| ostracize the doing-of-stuff successfully so the only way to
| survive or climb is to do stuff unsuccessfully (so they look
| good)
| epolanski wrote:
| You seem to be describing a company where bureaucracy is a
| feature not a bug.
|
| Been there. Left, live thousands times better.
| flakes wrote:
| > The documentation comments gets nitpicked to death with
| mostly useless comments about not having periods at the ends
| of lines > End up having to explain every little detail
| throughout the function
|
| For these cases I like to use the 'suggest an edit' feature
| on gitlab/github. Can have the change queued up in the
| comments and batch commit together, and takes almost no
| additional time/effort for the author. I typically add these
| suggestion comments and give an approve at the same time for
| small nitpicks, so no slow down in the PR process.
| MarkMarine wrote:
| I'm 15 years in and I feel basically the same. I end up
| making a feature or change, then going back and trying to
| split it into chunks that are digestible to my colleagues.
| I've got thousands of lines of staged changes that I'm
| waiting to drip out to people at a digestible pace.
|
| I yearn for the early stage startup where every commit is a
| big change and my colleagues are used to reviewing this, and
| I can execute at my actual pace.
|
| It's really changed the way I think about software in
| general, I've come around to Rich Hickey's radically simple
| language Clojure, because types bloat the refactors I'm
| doing.
|
| I'd love to have more of you where I work, is there some way
| I can see your work and send some job descriptions and see if
| you're interested?
| withinboredom wrote:
| > I end up making a feature or change, then going back and
| trying to split it into chunks that are digestible to my
| colleagues.
|
| If you are doing this AFTER you've written the code, it is
| probably way easier to do it as you go. It's one thing if
| you have no idea what the code will look like from the
| beginning -- just go ahead and open the big PR and EXPLAIN
| WHY. I know that I'm more than happy to review a big PR if
| I understand why it has to be big.
|
| I will be annoyed if I see a PR that is a mix of
| refactoring, bug fixes, and new features. You can (and
| should) have done those all as separate PRs (and tickets).
| If you need to refactor something, refactor it, and open a
| PR. It doesn't take that long and there's no need to wait
| until your huge PR is ready.
| quesomaster9000 wrote:
| Solving creative problems is often iterative, and one
| things I'm very concerned about when doing engineering
| management is maintaining momentum and flow. Looking at
| latency hierarchies is a really good example, you have
| registers, then cache, then memory, SSD, network etc. and
| consulting with another human asynchronously is like
| sending a message to Jupiter (in the best case).
|
| So, with an iterative process, the more times you
| introduce (at best) hour long delays, you end up sitting
| on your arse twiddling your thumbs doing nothing, until
| the response comes back.
|
| The concept of making PRs as you go fails to capture one
| of the aspects of low-latency problem solving, which is
| that you catch a problem, you correct it and you _revise
| it_ locally, without exiting that loop. Which is
| problematic because not only have you put yourself in a
| situation where you 're waiting for a response, but
| you've stopped half-way through an unfinished idea.
|
| This comes back to 'is it done', a gut feel that it's an
| appropriate time to break the loop and incur the latency
| cost, which for every developer will be different and is
| something that I have grown to deeply trust and and
| adjust to for everybody I work with.
|
| What I'm getting at is the iterative problem solving
| process often can't be neatly dissected into discrete
| units while it's happening, and after we've reached the
| 'doneness' point it takes much more work to undo part of
| your work and re-do it than it took to do originally, so
| not only do you have the async overhead of every
| interaction, but you have the cognitive burden of
| untangling what was previously a cohesive unit of thought
| - which again is another big time killer
| nradov wrote:
| It's the same with writing. The best authors occasionally break
| the rules of grammar and spelling in order to achieve a
| specific effect. But you have to learn the rules first, and
| break them only intentionally rather than accidentally.
| Otherwise your writing ends up as sloppy crap.
|
| (Of course some organizations have coding conventions that are
| just stupid, but that's a separate issue.)
| psychoslave wrote:
| If a function is longer than what I can display on a single
| screen, it better has to be argumented with very exceptional
| relevant requirements, which is just as straight forward to
| judge for anyone with a bit of experience.
| raincole wrote:
| I've seen a book promoting the idea that methods should not be
| longer than 5 lines.
|
| Of course now I know these ridiculous statements are from
| people hardly wrote any code in their lives, but if I'd read
| them at 18 I would have been totally misled.
| devjab wrote:
| Every SOLID, Clean Code, DRY and so on are all terrible advice
| sold by a bunch of people who haven't worked in software
| development since before Python was invented. Every one of those
| principles are continently vague so that people like Uncle Bob
| can claim that you got it wrong when it doesn't work for you.
| Uncle Bob is completely correct though, but maybe the reason you
| many others got it wrong is because the principles are
| continently vague. Continently because people like Uncle Bob are
| consultants who are happy to sell your organisation guidance. I
| think the biggest nail in the coffin of everything from TDD to
| Clean Architecture should be that they clearly haven't worked.
| It's been more than 20 years and software is more of a mess than
| if ever was. If all these "best practices" worked, they would
| have worked by now.
|
| YAGNI is the only principle I've seen consistently work. There
| are no other mantras that work. Abstractions are almost always
| terrible but even a rule like "if you rewrite it twice" or
| whatever people come up with aren't universal. Sometimes you want
| an abstraction from the beginning, sometimes you never want to
| abstract. The key is always to keep the cognitive load as low as
| possible as the author talks about. The same is true for small
| functions, and I've been guilty of this. It's much worse to have
| to go through 90 "go to definition" than just read through one
| long function.
|
| Yet we still teach these bad best practices to young developers
| under the pretence that it works and that everything else is
| technical debt. Hah, technical debt doesn't really exist. If you
| have to go back and replace part of your Python code with C
| because it's become a bottle neck that means you've made it. 95%
| of all software (and this number is angry man yelling at clouds)
| will never need to scale because it'll never get more than a few
| thousand users at best. Even if your software blows up chances
| are you won't know where the future bottle necks will be so stop
| trying to solve them before you run into them.
| jrs235 wrote:
| I think Cognitive Load on a developer includes
| distractions/interruptions. Constant slack notifications, taps on
| the shoulder, meetings, etc. increase cognitive load. It's
| context switching. One only has so much memory and focus, to
| switch tasks one has additional overhead, thinking and
| memory/storage demands.
| slowmovintarget wrote:
| Layering (properly) is used to manage dependencies. You isolate
| interface logic from business logic with data in between. It lets
| you evolve the architecture. This is a useful abstraction, not
| just something academic.
|
| https://www.destroyallsoftware.com/talks/boundaries
| mrcsd wrote:
| IMO cognative load is much easier to manage when required (human)
| memory use is less of a factor. In practical terms, this means
| maximising the locality of reasoning, i.e., having everything you
| need in front of you to make a decision. One of the reasons I
| favour rust is precisely because this factor has been a focus in
| the design.
| wcrichton wrote:
| I did my PhD at Stanford about the cognitive aspects of
| programming, including studies of cognitive load. This article
| uses pseudoscience to justify folk theories about programming. I
| would encourage readers to take everything with a grain of salt,
| and do not wave this article around as a "scientific"
| justification for anything.
|
| I laid out my objections to the article last year when it first
| circulated: https://github.com/zakirullin/cognitive-
| load/issues/22
| fforflo wrote:
| Congrats on your PhD at Stanford, but some humility has to be
| part of the scientific process for sure. It looks like a lot of
| folks called "programmers" agree with the points in the post.
| If it's such a common experience that should tell you something
| about the state of affairs.
|
| It's a blog post on the internet. Of course one should take it
| with a grain of salt. The same applies to any peer-reviewed
| article on software engineering for example.
|
| Just yesterday, I was watching this interview with Adam Frank
| [0] one of the parts that stood out was his saying why "Why
| Science Cannot Ignore Human Experience" (I can't find the exact
| snippet, but apparently he has a book with the same title.
|
| [0] https://www.youtube.com/watch?v=yhZAXXI83-4
| GiorgioG wrote:
| Where was the word "scientific" mentioned in the article? I
| don't think you were the target reader they had in mind when
| the author wrote this. I've been programming since 1986, and
| this article resonates with my experience. More abstractions,
| layers, and so on requires my brain to have to keep track of
| more shit which takes away from doing the work that brought me
| to work on that bit of code (bug fix, feature work, debugging,
| etc.)
|
| We're very proud of you and the hard work you did to earn your
| PhD, now please stop trotting it out.
| sgt wrote:
| AdminController extends UserController extends GuestController
| extends BaseController
|
| That's nothing... _Java enterprise programmer enters the chat_
| creer wrote:
| > Too many small methods, classes or modules > Method, class and
| module are interchangeable in this context
|
| Class, method, functions are NOT the only way to manage cognitive
| load. Other ways work well for thinking developers:
|
| Formatting - such as a longer lines and lining up things to
| highlight identical and different bits.
|
| Commenting - What a concept?! using comments to make things more
| clear.
|
| Syntactic sugar, moderate use of DSL features, macros... - Is
| this sometimes the right way?
|
| But yeah, if your tool or style guide or programming language
| even, imposes doing everything through the object system or
| functions, then someone clearly knew better. And reduced your
| cognitive load by taking away your choices /s.
| svilen_dobrev wrote:
| > Involve junior developers in architecture reviews. They will
| help you to identify the mentally demanding areas.
|
| This.
|
| And also, a mantra of my own: Listen _carefully_ to any newcomer
| in the team /company in first 1-3 weeks, until s/he gets
| accustomed (and/or stop paying attention to somewhat uneasy
| stuff). They will tell you all things that are, if not wrong, at
| least weird.
| tantalor wrote:
| Junior developers probably won't say anything, because they are
| used to not understanding code, and they are not going to
| second guess the more-experienced author.
| jstummbillig wrote:
| It's definitely on the senior to prompt the junior
| appropriately. But when you do, they will.
| ben_w wrote:
| Aye, but the joiners may need prompting as well as getting
| listened to.
|
| In each place where I've seen something wildly wrong, the
| problem has been clear in the first few weeks -- sometimes even
| in the first few days* -- but I always start with the
| assumption that if I disagree with someone who has been at it
| for years, they've probably got good reasons for the stuff that
| surprises me.
|
| Unfortunately I'm not very convincing: when I do finally feel
| confident enough to raise stuff, quite often they do indeed
| have reasons... bad reasons that ultimately prove to be fatal
| or near-fatal flaws to their business plans, but the issues
| only seldom get fixed once I raise them.
|
| * one case where the problem was visible in the interview, but
| I was too young and naive so I disregarded what I witnessed,
| and I regretted it.
| epolanski wrote:
| I second this, but one should also be extremely wary of
| newcomers feedback and try to understand their nature.
|
| Some people are extremely resistant to new ideas, some might be
| simply lazy, some can't be bothered to read documentations,
| etc.
|
| Spotting the real person behind the feedback is crucial and
| often those people need to be fired fast.
|
| I myself tend to be lazy when it comes to learn new
| stuff/patterns, especially when I am in the middle of having to
| progress a project so my own feedback may be more of a
| frustration for my inability to progress due to having to
| understand first a, b, c and d which may take considerable time
| and pain for something I can do in an old way in few minutes.
| bob1029 wrote:
| > Types of cognitive load - Extrinsic/Intrinsic
|
| This neatly mirrors the central ideas presented in Out of the Tar
| Pit [0], which defines accidental and essential complexity.
|
| Reading this paper was probably one of the biggest career unlocks
| for me. You really can win ~the entire game if you stay focused
| on the schema and keep in touch with the customer often enough to
| ensure that it makes sense to them over time.
|
| OOTP presents a functional-relational programming approach, but
| you really just need the relational part to manage the complexity
| of the domain. Being able to say that one domain type is relevant
| to another domain type, but only by way of a certain set of
| attributes (in a 3rd domain type - join table), is an
| unbelievably powerful tool when used with discipline. This is how
| you can directly represent messy real world things like circular
| dependencies. Modern SQL dialects provide recursive CTEs which
| were intended to query these implied graphs.
|
| Over time, my experience has evolved into "let's do as much
| within the RDBMS as we possibly can". LINQ & friends are
| certainly nice to have if you need to build a fancy ETL pipeline
| that interfaces with some non-SQL target, but they'll never beat
| a simple merge statement in brevity or performance if the source
| & target of the information is ultimately within the same DB
| scope. I find myself spending more time in the SQL tools (and
| Excel) than I do in the various code tools.
|
| [0] https://curtclifton.net/papers/MoseleyMarks06a.pdf
| jmyeet wrote:
| I have an issue with complex conditions with or without local
| variable labels for readability. You really shouldn't have them
| at all.
|
| At one time, they used to teach that functions should have one
| entry-point (this is typically a non-issue but can come up with
| assembly code) and one exit-point. Instead of a complex
| condition, I much prefer just early returns ie:
| // what's going on 1 if (condition1 || condition2) {
| return; } // what's going on 2 if
| (condition3 && condition4) { return; }
| // what's going on 3 if (condition5) { return;
| } // do the thing return;
| Aeolos wrote:
| I prefer this style for languages that have either a GC or
| scoped resource management (eg RAII).
|
| However, I think the single exit point holds merit for C, where
| an early return can easily and silently cause a resource leak.
| (Unless you use compiler-specific extensions, or enforce rust-
| style resource ownership, which is really hard without compiler
| support.)
| meowface wrote:
| I've sometimes seen people attack early returns and I've never
| understood it. To me they make things so much cleaner that it
| seems like common sense.
| worik wrote:
| Interesting. I am in agreement
|
| But not one word about comments, and only one about naming.
|
| Useful comments go a long way to lessening cognitive load
|
| Good names are mnemonics, not documentation
|
| I have worked on code bases with zero comments on the purposes of
| functions, and names like "next()"
|
| And I've worked with programmers who name things like
| "next_stage_after_numeric_input"
| K0nserv wrote:
| I've been thinking about the notion of "reasoning locally"
| recently. Enabling local reasoning is the only way to scale
| software development past some number of lines or complexity.
| When reasoning locally, one only needs to understand a small
| subset, hundreds of lines, to safely make changes in programs
| comprising millions.
|
| I find types helps massively with this. A function with well-
| constrained inputs and outputs is easy to reason about. One does
| not have to look at other code to do it. However, programs that
| leverage types effectively are sometimes construed as having high
| cognitive load, when it in fact they have low load. For example a
| type like `Option<HashSet<UserId>>` carries a lot of
| information(has low load): we might not have a set of user ids,
| but if we do they are unique.
|
| The discourse around small functions and the clean code
| guidelines is fascinating. The complaint is usually, as in this
| post, that having to go read all the small functions adds
| cognitive load and makes reading the code harder. Proponents of
| small functions argue that you don't have to read more than the
| signature and name of a function to understand what it does; it's
| obvious what a function called last that takes a list and returns
| an optional value does. If someone feels compelled to read every
| function either the functions are poor abstractions or the reader
| has trust issues, which may be warranted. Of course, all
| abstractions are leaky, but perhaps some initial trust in `last`
| is warranted.
| 0xFACEFEED wrote:
| > A function with well-constrained inputs and outputs is easy
| to reason about.
|
| It's quite easy to imagine a well factored codebase where all
| things are neatly separated. If you've written something a
| thousand times, like user authentication, then you can plan out
| exactly how you want to separate everything. But user
| authentication isn't where things get messy.
|
| The messy stuff is where the real world concepts need to be
| transformed into code. Where just the _concepts_ need to be
| whiteboarded and explained because they 're unintuitive and
| confusing. Then these unintuitive and confusing concepts need
| to somehow described to the computer.
|
| Oh, and it needs to be fast. So not only do you need to model
| an unintuitive and confusing concept - you also need to write
| it in a convoluted way because, for various annoying reasons,
| that's what performs best on the computer.
|
| Oh, and in 6 months the unintuitive and confusing concept needs
| to be completely changed into - surprise, surprise - a
| completely different but equally unintuitive and confusing
| concept.
|
| Oh, and you can't rewrite everything because there isn't enough
| time or budget to do that. You have to minimally change the
| current uintuitive and confusing thing so that it works like
| the new unintuitive and confusing thing is supposed to work.
|
| Oh, and the original author doesn't work here anymore so no
| one's here to explain the original code's intent.
| haliskerbas wrote:
| This puts things really well. I'll add into it that between
| the first white boarding session and the first working MVP
| there'll be plenty of stakeholders who change their mind,
| find new info, or ask for updates that may break the original
| plan
| ilvez wrote:
| Plus to everything said. It's an everyday life of
| "maintainer", picking the next battle to pick the best way to
| avoid sinking deeper and defending the story that exactly
| "this" is the next refactoring project. All that while
| balancing different factors as you mention to actually
| believe oneself, because there are countless of paths..
| mgkimsal wrote:
| > Oh, and in 6 months the unintuitive and confusing concept
| needs to be completely changed into - surprise, surprise - a
| completely different but equally unintuitive and confusing
| concept.
|
| But you have to keep the old way of working exactly the same,
| and the data can't change, but also needs to work in the new
| version as well. Actually show someone there's two modes, and
| offer to migrate their data to version 2? No way - that's
| confusing! Show different UI in different areas with the same
| data that behaves differently based on ... undisclosed-to-
| the-user criteria. That will be far less confusing.
| terribleperson wrote:
| As a user 'intuitive' UIs that hide a bunch of undisclosed
| but relevant complexity send me into a frothing rage.
| motorest wrote:
| > I've been thinking about the notion of "reasoning locally"
| recently. Enabling local reasoning is the only way to scale
| software development past some number of lines or complexity.
| When reasoning locally, one only needs to understand a small
| subset, hundreds of lines, to safely make changes in programs
| comprising millions.
|
| That was supposedly the main trait of object-oriented
| programming. Personally that was how it was taught to me: the
| whole point of encapsulation and information hiding is to
| ensure developers can "reason locally", and thus be able to
| develop more complex projects by containing complexity to
| specific units of execution.
|
| Half of SOLID principles also push for that. The main benefit
| of Liskov's substitution principle is ensure developers don't
| need to dig into each and every concrete implementation to be
| able to reason locally about the code.
|
| On top of that, there are a multitude of principles and rules
| of thumb that also enforce that trait. For example, declaring
| variables right before they are used the first time. Don't
| Repeat Yourself to avoid parsing multiple implementations of
| the same routine. Write Everything Twice to avoid premature
| abstractions and tightly coupling units of execution that are
| actually completely independent, etc etc etc.
|
| Heck, even modularity, layered software architectures, and even
| microservices are used to allow developers to reason locally.
|
| In fact, is there any software engineering principle that isn't
| pushing for limiting complexity and allowing developers to
| reason locally?
| hiAndrewQuinn wrote:
| In theory, you could design a parallel set of software
| engineering best practices which emphasize long-term memory
| of the codebase over short-term ability to leaf through and
| understand it. I guess that would be "reasoning nonlocally"
| in a useful sense.
|
| In practice I think the only time this would be seen as a
| potentially good thing by most devs is if it was happening in
| heavily optimized code.
| DarkNova6 wrote:
| 100% agree and this not only concerns readability. The concept
| of "locality" turns out to be a fairly universal concept, which
| applies to human processes just as much as technical ones.
| Side-effects are the root of all evil.
|
| You don't see a waiter taking orders from 1 person on a table,
| but rather go to a table and get orders from everybody sitting
| there.
|
| And as for large methods, I find that they can be broken into
| smaller once just fine as long as you keep them side-effect
| free. Give them a clear name, a clear return value and now you
| have a good model for the underlying problem you are solving.
| Looking up the actual definition is just looking at
| implementation details.
| LtWorf wrote:
| I've seen functions called getValue() that were actually
| creating files on disk and writing stuff.
|
| Also, even if the function actually does what advertised, I've
| seen functions that go 4-5 levels deep where the outer
| functions are just abstracting optional parameters. So to avoid
| exposing 3 or 4 parameters, tens of functions are created
| instead.
|
| I think you do have a point but ideas get abused a lot.
| bb88 wrote:
| The larger problem are things that have global effect:
| databases, caches, files, static memory, etc. Or protocols
| between different systems. These are hard to abstract away,
| usually because of shared state.
| papaver wrote:
| a lot of good points but i feel like one of the biggest i've
| learned is missing...
|
| leaning toward functional techniques has probably had the biggest
| impact on my productivity in the last 10 years. some of the
| highest cognitive load in code comes from storing the current
| state of objects in ones limited memory. removing state and
| working with transparent functions completely changes the game.
| once i write a function that i trust does its job i can replace
| its implementation with its name in my memory and move on to the
| next one.
| menotyou wrote:
| Before OOP became popular the usage of global variables was
| discouraged in procedural languages because it was the cause of
| many bugs and errors.
|
| In OOP global state variables were renamed to instance
| variables and are now widely used. The problem why it was
| discouraged beforehand did not went away by renaming but is now
| spread all over the place.
| markus_zhang wrote:
| I don't program professionally (I work as a DE but I don't
| consider it as a serious programming venue) so the No. 1 issue
| while reading medium-large source code is abstraction --
| programming patterns.
|
| I hope it improves whence I write an implementation myself.
| jeremydeanlakey wrote:
| The central idea around cognitive load is very good, central to
| writing good code.
|
| But it's deeply mistaken to oppose smaller (or more correctly:
| simpler) classes/functions and layered architecture.
|
| Layered architecture and simple (mostly small) classes and
| methods are critical to light cognitive load.
|
| e.g. You should not be handling database functionality in your
| service classes, nor should you be doing business logic in your
| controllers. These different kinds of logic are very different,
| require different kinds of knowledge. Combining them _increases_
| cognitive load, not decreases.
|
| It's not mainly about swapping out dependencies (although this is
| an important benefit), it's about doing one thing at a time.
| jeremydeanlakey wrote:
| To make this more concrete:
|
| If your service layer method requires data to be saved and the
| results to be sorted, you want to call a data layer method that
| saves it and a library method that sorts it. You do not want
| any of that saving or sorting functionality in your service
| method.
|
| Combining different layers and different tasks so that your
| module is "deep" rather than "shallow" will make your code much
| higher cognitive load and create a lot of bugs.
| James_K wrote:
| I hate these silly little code examples where they show some if
| condition being changed to another format and act as if that's
| real advice that helps you program.
| jakobov wrote:
| Yes 100%!
|
| Read "Code is For Humans" for more on the subject
| https://www.amazon.com/dp/B0CN6PQ42B
| halfcat wrote:
| I suspect part of the challenge is we're dealing with a graph (of
| execution paths) but all we have to work with is a tree (file
| system).
|
| Every person will prefer a different grouping the execution paths
| that lowers their cognitive load. But for any way you group
| execution paths, you exclude a different grouping that would have
| been beneficial to someone working at a different level of
| abstraction.
|
| So you like your function that fits in one computer screen, but
| that increases the cognitive load on someone else who's working
| on a different problem that has cross-cutting concerns across
| many modules. If you have separate frontend/backend teams you'll
| like Rails, but a team of full stack people will prefer Django
| (just because they group the same things differently).
|
| I guess this is just Conway's law again?
| DarkNova6 wrote:
| > Layered architecture: Abstraction is supposed to hide
| complexity, here it just adds indirection
|
| Agree with everything except this. As someone who deals with
| workflows and complex business domains, separating your technical
| concerns from your core domain is not only necessary. They are a
| key means to survival.
|
| Once you have 3 different input-channels and 5 external systems
| you need to call, you absolutely need to keep your distance not
| to pollute your core representation of the actual problem you
| need to solve.
___________________________________________________________________
(page generated 2024-12-25 23:00 UTC)