11 ms·
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
by K0nserv 2y ago
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 2y ago> 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 2y agoThis 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 2y agoPlus 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 2y ago> 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 2y agoAs a user 'intuitive' UIs that hide a bunch of undisclosed but relevant complexity send me into a frothing rage.
- robocat 2y agoI'm trying to learn acceptance: how not to get so angry at despicable UIs. Although I admit I'm kinda failing. My minor successes have been by avoiding software: e.g. giving up programming (broken tools and broken targets were a major frustration) and getting rid of Windows.
- motorest 2y ago> 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 2y agoIn 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.
- K0nserv 2y agoAn interesting point. Would there be any benefits to this non-local reasoning?
- hiAndrewQuinn 2y ago
- DarkNova6 2y ago100% 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 2y agoI'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 2y agoThe 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.
- mrkeen 2y agoWeird, I read that between the lines of parent's post. Of course local reasoning precludes global effects.
- peterlada 2y agoThere is an issue of reading a code that is written by somebody else. If it's not in a common style, the cognitive load of parsing how it's done is an overhead. The reason I used to hate Perl was around this, everyone had a unique way of using Perl and it had many ways to do the same thing. The reason I dislike functional programming is around the same, you can skin the cat 5 ways, then all 5 engineers will pick a different way of writing that in Typescript. The reason I like Python more is that all experienced engineers will eventually gravitate towards the idea of Pythonic notion and I've had colleagues whose code looked identical to how I'd have written it.
- mrkeen 2y agoPython 2, Python 3? Types or no types?
- sunshowers 2y agoThis is absolutely the right way to think about things. I like thinking about local reasoning in terms of (borrowing from Ed Page) "units of controversy". For example, I like using newtypes for identifiers, because "what strings are permitted to be identifiers" is a unit of controversy.
- nevi-me 2y ago> If someone feels compelled to read every function either the functions are poor abstractions or the reader has trust issues, which may be warranted. I joined a company with great code and architecture for 3 months last year. They deal with remittances and payments. Their architecture leads are very clued up, and I observed that they spent a lot of quality time figuring out their architecture and improvements, continuously. They'd do a lot of refactors for all the various teams, and the cadence of feature development and release was quite impressive. In that period though, I and another long-standing colleague made a few errors that cost the company a lot of money, like an automated system duplicating payments to users for a few hours until we noticed it. Part of their architectural decision was to use small functions to encapsulate logic, and great care and code review was put into naming functions appropriately (though they were comment averse). The mistakes we committed, were because we trusted that those functions did what they said they did correctly. After all, they've also been unit tested, and there's also integration tests. If it weren't for the fortitude of the project manager (great guy hey) in firmly believing in collective responsibility if there's no malice, I'd probably have been fired after a few weeks (I left for a higher offer elsewhere). --- So the part about trust issues resonates well with me. As a team we made the decision that we shouldn't always trust existing code, and the weeks thereafter had much higher cognitive load.
- avg_dev 2y agoThat sounds like a very difficult situation. Would you be willing to elaborate on what kinds of bugs lay in the pre-existing functions? Was some sort of operation that was supposed to be idempotent (“if you call it with these unique parameters over and over, it will be the same as if you only called it once”) not so? I am trying to imagine what went wrong here. A tough situation, must have been quite painful. How serious were the consequences? If you don’t feel comfortable answering that is okay.
- nevi-me 2y agoI can't remember the exact detail, but one instance was a function checking whether a user should be paid based on some conditions. It checked the db, and I think because the codebase and db move fast, there was a new enum added a few months prior which was triggered by our transaction type. So that helped function didn't account for the new enum, and we ended up sending >2 payments to users, in some cases I think over 10 to one user. The issue was brought to customer support's attention, else we might have only noticed it at the end of the week, which I think would have led to severe consequences. The consequences never reached us because our PM dealt with them. I suppose in all the financial loss instances, the business absorbed the losses.
- casenmgreen 2y agoI may be wrong, but my view of software is : you have functions, and you have the order in which functions are called. Any given function is straightforward enough, if you define its function clearly and keep it small enough - both of which can reasonably be done. Then we have the problem, which is the main problem, of the order in which functions are called. For this, I use a state machine. Write out the state machine, in full, in text, and then implement it directly, one function per state, one function per state transition. The SM design doc is the documentation of the order of function calling, it is exhaustive and correct, and allows for straightforward changes in future (at least, as straightforward as possible - it is always a challenge to make changes).
- euph0ria 2y agoWould love to understand this better. Is there any example you could point to?
- casenmgreen 2y agoinit -> success -> red init -> failure -> cleanup red -> success -> red_yellow red -> failure -> cleanup red_yellow -> success -> green red_yellow -> failure -> cleanup green -> success -> yellow green -> failure -> cleanup yellow -> success -> red yellow -> failure -> cleanup cleanup -> done -> finish init/red/etc are states. success/failure/etc are events. Each state is a function. The function red() for example, waits for 20 seconds, then returns success (assuming nothing went wrong). To start the state machine, initializes state to "init", and enter a loop, in the loop you call the function for the current state (which makes that state actually happen and do whatever it does), and that function returns its event for whatever happen when it was run, and you then call a second function, which updates state based on the event which just occurred. Keep doing that, until you hit state "finish", then you're done.
- euph0ria 2y agoGot it, thanks. But it seemed from your original post that you tend to write state machines a lot more than the usual engineer does, would that be correct? Would you use this in a crud rest API for example?
- jreback 2y agoRe: trust issues...I'd argue this is the purpose of automated tests. I think tests are too often left out of architectural discussions as if they are some additional artifact that gets created separately from the running software. The core / foundational / heavily reused parts of the architecture should have the most tests and ensure the consumers of those parts has no trust issues!
- K0nserv 2y agoTests are good but moving left by lifting invariants into the type system is better. Compare fn send_email(addr: &str, subject: &str, body: &str) -> Result<()> to fn send_email(add: &EmailAddr, subject: &str, body: &str) -> Result<()> In the second case, the edge cases of an empty or invalid email address don't need to be tested, they are statically impossible.
- galangalalgol 2y agoThanks for the small concrete example. I try to explain this a lot. It also makes coverage really easy to get with fewer tests.
- mattmcknight 2y agoTypes? "Option<HashSet<UserId>>" means almost nothing to me. A well defined domain model should indicate what that structure represents.
- bbkane 2y agoEven that means a lot more than `{}`, who's tortured journeys I have to painstakingly take notes om in the source code while I wonder what the heck happened to produce the stack trace...
- mattmcknight 2y agoYes, but it means less than something like UserGroup. I hear you on {} though, I'm currently looking at "e: ".
- jbggs 2y agoit seems like you're just not familiar with the domains defined by those types, or at least the names used here
- mattmcknight 2y agoAre you? What would an option for a hash set of userids? I just don't find that "types" magically solve problems of cognitive load.
- K0nserv 2y ago> What would an option for a hash set of userids? As an argument: An optional filter for a query e.g. "return me posts form these users" As a return value: The users who liked a post or nothing if it's not semantically valid for the post to be liked for some reason. > I just don't find that "types" magically solve problems of cognitive load. Cognitive load is about working memory and having to keep things in it. Without types one only has a name, say "userIds". The fact that it's possible for it to be null and that it's supposed to contain unique values has to be kept in working memory(an increase in cognitive load)
- ratmeadow 2y agoIn regards to small functions, I think an important - but not often mentioned - aspect is shared assumptions. You can have many small functions with garbage abstractions that each implictly rely on the behaviour of each other - therefore the cognitive load is high. Or, you can have many small functions which are truly well-contained, in which case you may well need not read the implementation. Far too much code falls into the former scenario, IMO.
- john61 2y agoI do not agree that typing leads to less cognitive load. Typing often leads to more and more complicated code. Dynamically typed code is often shorter and more compact. If dynamically typed code is well written, its function, inputs and outputs are clear and obvious. Clear and easy to understand code is not primarily a matter of typed or not typed code, it is a matter of a great programmer or a poor one.
- mirekrusin 2y agoThis is true at smaller scales and flips over on larger scales (larger codebase, dependencies, team/teams sizes).
- john61 2y agoA function is clear or not. I fail to see how the scale of the code, team, dependence is a factor in that.
- K0nserv 2y agoI split local reasoning into horizontal or vertical. Vertical reasoning is reasoning inside a module or function. Here information hiding and clear interfaces help. Horizontal reasoning is reasoning across the codebase in a limited context; adding a new parameter to a public function is a good example. The compiler helps you find and fix all the use sites, and with good ability to reason vertically at each site, even a change like this is simple.
- K0nserv 2y agoStatically typed code definitely requires more effort to read, but this is not cognitive load. Cognitive load is about how much working memory is required. Statically typed code requires less cognitive load because some of the remembering is outsourced to the source code. Statically typed code can lead to more complicated code; it can also accurately reflect the complexity inherent in the problem.
- com2kid 2y agoThere is a function. It takes in 4 parameters. One of them is called ID Is ID a string, a number, a GUID? Better check the usage within the function. Oh, the declaration is `id: number` Mystery solved. Even better if the language supports subtyping so it is something like id: userID and userID is a subtype of number.
- dataflow 2y agoAbsolutely with you on the idea in the abstract, but the problem you run into in practice is that enabling local reasoning (~O(1)-time reading) often comes at the cost of making global changes (say, ~O(n)-time writing in the worst case, where n is the call hierarchy size) to the codebase. Or to put it another way, the problem isn't so much attaining local readability but maintaining it -- it imposes a real cost on maintenance. The cost is often worth it, but not always. Concrete toy examples help here, so let me just give a straight code example. Say you have the following interface: void foo(void on_completed()); void callback(); void bar(int n) { foo(callback); } Now let's say you want to pass n to your callback. (And before you object that you'd have the foresight to enable that right in the beginning because this is obvious -- that's missing the point, this is just a toy example to make the problem obvious. The whole point here is you found a deficiency in what data you're allowed to pass somewhere, and you're trying to fix it during maintenance. "Don't make mistakes" is not a strategy.) So the question is: what do you do? You have two options: 1. Modify foo()'s implementation (if you even can! if it's opaque third party code, you're already out of luck) to accept data (state/context) along with the callback, and plumb that context through everywhere in the call hierarchy. 2. Just embed n in a global or thread-local variable somewhere and retrieve it later, with appropriate locking, etc. if need be. So... which one do you do? Option #1 is a massive undertaking. Not only is it an O(n) changes for a call hierarchy of size n, but foo() might have to do a lot of extra work now -- for example, if it previously used a lock-free queue to store the callback, now it might lose performance as it might not be able to do everything atomically. etc. Option #2 only results in 3 modifications, completely independently from the rest of the code: one in bar(), one for the global, and one in the callback. Of course the benefit of #1 here is that option #1 allows local reasoning when reading the code later, whereas option #2 is spooky action at a distance: it's no longer obvious that callback() expects a global to be set. But the downside is that now you might need to spend several more hours or days or weeks to make it work -- depending on how much code you need to modify, which teams need to approve your changes, and how likely you are to hit obstacles. So, congratulations, you just took a week to write something that could've taken half an hour. Was it worth it? I mean, probably yes, if maintenance is a rare event for you. But what if you have to do it frequently? Is it actually worth it to your business to make (say) 20% of your work take 10-100x as long? I mean, maybe still it is in a lot of cases. I'm not here to give answers, I absolutely agree local reasoning is important. I certainly am a zealot for local reasoning myself. But I've also come to realize that achieving niceness is quite a different beast from maintaining it, and I ~practically never see people try to give realistic quantified assessments of the costs when trying to give advice on how to maintain a codebase.
- ajuc 2y agoLast is something that is embarrassingly extractable, which makes it a bad example (you shouldn't write that function anyway in 99% of cases - surely someone wrote it already in stdlib of your language). It's like taking "list.map(x -> x*x)" as a proof that parallelism is easy. Most code is not embarrassingly extractable (or at least not at granularity of 3 lines long methods).
- movpasd 2y agoI feel that one big way in which engineers talk past each other is in assuming that code quality is an inherent property of the code itself. The code is meaningless without human (and computer) interpretation. Therefore, the quality of code is a function of the relationship between that code and its social context. Cognitive load is contextual. `Option<HashSet<UserId>>` is readable to someone knowledgeable in the language (`Option`, `HashSet`) and in the system (meaning of `UserId` -- the name suggests it's an integer or GUID newtype, but do we know that for sure? Perhaps it borrows conventions from a legacy system and so has more string-like semantics? Maybe users belong to groups, and the group ID is considered part of the user ID -- or perhaps to uniquely identify a user, you need both the group and user IDs together?). What is the cognitive load of `Callable[[LogRecord, SystemDesc], int]`? Perhaps in context, `SystemDesc` is very obvious, or perhaps not. With surrounding documentation, maybe it is clear what the `int` is supposed to mean, or maybe it would be best served wrapped in a newtype. Maybe your function takes ten different `Callable`s and it would be better pulled out into an polymorphic type. But maybe your language makes that awkward or difficult. Or maybe your function is a library export, or even if it isn't, it's used in too many places to make refactoring worthwhile right now. I also quite like newtypes for indicating pragmatics, but it is also a contextually-dependent trade-off. You may make calls to your module more obvious to read, but you also expand the module's surface area. That means more things for people writing client code to understand, and more points of failure in case of changes (coupling). In the end, it seems to me that it is less important whether you use a newtype or not, and more important to be consistent. In fact, this very trade-off -- readability versus surface area -- is at the heart of the "small vs large functions" debate. More smaller functions, and you push your complexity out into the interfaces and relationships between functions. Fewer large functions, and the complexity is internalised inside the functions. To me, function size is less the deciding factor [0], but rather whether your interfaces are real, _conceptually_ clean joints of your solution. We have to think at a system level. Interfaces hide complexity, but only if the system as a whole ends up easier to reason about and easier to change. You pay a cost for both interface (surface area) and implementation (volume). There should be a happy middle. --- [0] Also because size is often a deceptively poor indicator of implementation complexity in the first place, especially when mathematical expressions are involved. Mathematical expressions are fantastic exactly because they syntactically condense complexity, but it means very little syntactic redundancy, and so they seem to be magnets for typos and oversights.
- TZubiri 2y ago>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. Have you never heard of the word of our lord and saviour oop, or functions? It's called encapsulation. You might have learned it through prog langs as it is an embedded ideal
- K0nserv 2y agoAs another sibling comment pointed out there are many tools that enable local reasoning, encapsulation is one such tool. I'm not claiming the idea is novel, just that I haven't encountered a name for it before.
- TZubiri 2y agoI'm not saying that encapsulation is a tool for local reasoning, I'm saying they are the same concept. How is the concept of local reasoning distinct from that of encapsulation?
- galangalalgol 2y agoI think most of us associate the word encapsulation with OOP nightmare code that spread mutable state across many small classes that often inherited from one another and hid the wrong state. Stateless and low state are the reaction to that. If you expand the term to include those aids to local reasoning then many more might agree with you.
- konschubert 2y agoThe first step for allowing local reasoning is to break your product into independent subdomains that are as independent as possible. For a software company, this means crafting the product ownership of your team such that the teams can act as independently as possible. This is where most companies already fail. Once this has been achieved, you can follow this pattern on smaller and smaller scales down to individual functions in your code.
- larsrc 2y ago> If someone feels compelled to read every function either the functions are poor abstractions or the reader has trust issues, which may be warranted. Or it's open source and the authors were very much into Use The Source, Luke!
- tdiff 2y ago> 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. It's also interesting that in comment to the same article many people argue against PR process. I hardly see how else that level of discipline required not to undermine trust in names of small methods can be maintained for any team with more than 3 developers.
- blago 2y ago> 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; Although this is often the case, the style of the program can change things significantly. Here are a few, not so uncommon, examples where it starts to break down: 1. When you’re crafting algorithms, you might try to keep code blocks brief, but coming up with precise, descriptive names for each 50-line snippet can be hard. Especially if the average developer might not even fully understand the textbook chapter behind it. 2. At some point you have to build higher than "removeLastElementFromArray"-type of functions. You are not going to get very far skimming domain-specific function names if don’t have any background in that area. More examples exist, but these two illustrate the point.
- K0nserv 2y agoBoth examples stem from not understanding the problem well enough I think. My best work is done when I first write a throwaway spaghetti solution to the problem. Only through this endeavour do I understand the problem well enough to effectively decompose the solution.
- nuancebydefault 2y agoYou understand your final fine grained code after your 'spaghetti' intermezzo. Others and your future you, probably less so.
- K0nserv 2y agoMy point is that the factoring and abstractions one produce after the spaghetti intermezzo will be better than a blind stab at them; a greater understanding of the problem helps.
- nuancebydefault 2y agoAgree that intermezzos - even of the spaghetti kind - help understanding. I thought this thread was more about (non) maintainability of code consisting of many procedures for each of which names are to be found that will make their usage self-explaining. From my experience, simple API's with complex and often long implementations can be very well suited. As long as those are low on side effects and normally DRY, as opposed to puristicly DRY.
- meehai 2y agoAt work we have a pretty big Python monorepo. The way we scale it is by having many standalone CLI mini apps ( about 80) atm with most of them outputting json/parquet in GCS or bigquery tables. Inputs are the same. I insisted a lot on this unix (ish as it's not pipes) philosophy. It paid off so far. We can test each cli app as well as make broader integration tests.
- cle 2y ago> 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. They sometimes help. But I think it's deeper than this. A function with inputs and outputs that are well-constrained with very abstract, complex types is still hard to reason about, unless you're used to those abstractions. I think it's more accurate to say that something is "easy to reason about" if its level of abstraction "closely matches" the level of abstraction your brain is comfortable with / used to. This can vary dramatically between people, depending on their background, experience, culture, etc. I could describe the Option<HashSet<UserId>> type in terms of functors and applicatives and monads, and though it would describe exactly the same set of valid values, it has a much higher cognitive load for most people. > However, programs that leverage types effectively are sometimes construed as having high cognitive load, when it in fact they have low load. Cognitive load is an individual experience. If someone "construes" something as having high cognitive load, then it does! (For them). We should be writing programs that minimize cognitive load for the set of programmers who we want to be able to interact w/ the code. That means the abstractions need to sufficiently match what they are comfortable with. It's also fine to say "sorry, this code was not intended to have low cognitive load for you".
- mithametacs 2y agoNot everything is a functional program though and side effects are important. Types can’t* represent this. *Not for practical programs
- jonahx 2y ago> 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. I used to be one of those proponents, and have done a 180. The problems are: 1. The names are never as self-evident as you think, even if you take great care with them. 2. Simply having so many names is an impediment in itself. The better way: Only break things up when you need to do. This means the "pieces" of the system correspond to the things you care about and are likely to change. You'll know where to look. When you actually need an abstraction to share code between parts of the system, create it then.
- hinkley 2y agoTypes are somewhat a different dimension. Sort of the classic 1 dimensional argument about a 2 dimensional problem domain. Which quadrant you’re talking about alters whether the arguments support reality or argue with it. If understanding a block of code requires knowing a concept that the team feels everyone should know anyway, then it’s not such an imposition. If the code invites you to learn that concept, so much the better. The code is “discoverable” - it invites you to learn more. If the concept is incidental to the problem and/or the team is objectively wrong in their opinion, then you have tribal knowledge that is encroaching on the problem at hand. And whether it’s discoverable or not is neither here nor there. Because understanding the code requires knowing lots of other things, which means either memorization, or juggling more concepts than comfortably fit in short term memory - cognitive overload. You know you’ve blown past this point when you finally trace the source of a bad piece of data but cannot remember why you were looking for it in the first place. I’m hoping the problem of cognitive load gets more attention in the near future. We are overdue. But aside from people YouTubing code reviews, I’m still unclear what sorts of actionable metrics or feedback will win out in this arena. Maybe expanding code complexity to encompass the complexity of acquiring the values used in the code, not just the local data flow.