I don't necessarily agree with the step of putting the code in a separate function; that often works, but just as often makes it so that the code can't be read top-to-bottom anymore which hurts readability.
In this case there's, I think, a better alternative; the equivalent-ish code in Ruby for the example code here would be something like this:
values = s
.partition('?')[-1]
.split('&')
.map { |key_value| key_value.partition('=')[-1] }
You can write these nice functional pipelines where you just read the code top-to-bottom and see step-by-step what is being done to the data on each line. You don't have to jump up-and-down around the code when reading it, and you don't have to keep too much context in your head when reading it.
This is one of the reasons why I vastly prefer Ruby over Python for most data processing tasks. I wish more languages would support this style of programming.
The idea of the technique is to split out code at a different level of abstraction with a clear name communicating what it does, while hiding the details of the how, because you don't need to care about that detail at all to fully grok the code in the calling function.
Where this breaks down is when the code you're trying to split out is not at a different level of abstraction, and how it works is meaningful to the surrounding code in the calling function.
So I think the issue you are seeing isn't with the technique, it's with the technique being misapplied. I think this is likely the only difference between when it 'often works' and 'just as often doesn't' in the code you're working in :-)
Each function becomes something new that needs to stick in your brain.
Someone that applies "MORF" to their code winds up nearly inventing their own language in the file that they're writing. All that takes up more memory when you're reading their code, because due to leaky abstractions the actual implementation of whatever the function name that you replace it with is often important.
I have an actual track record of taking code that someone had MORF'd to hell and rewriting it, and making it about 40% shorter, with much fewer concepts to process.
Inventing a term like "MORF" is probably illustrative of the problem itself. Without looking at the blog post what exactly was that acronym again? That is just one more thing for you to try to memorize. The author is riffing on things like "DRY" and "YAGNI" that are well-known, but it isn't really helping with readability when you lift it out of that context.
> Each function becomes something new that needs to stick in your brain.
But if you don't put it in a separate function, then all that code becomes something that you have no choice but look at as part of this function, because the text of it is right there in the function.
Sticking part of the code in a sub-function gives you the choice: Do I trust that this function does what it says, with no other effects that I have to worry about? Or do I go look at it to make sure?
But not sticking the code in a sub-function gives you no choice - it's right there as part of the function you're looking at, and you have to see what that code is doing as part of understanding the function. And having that code not in a separate function makes it more likely that it has side effects that can mess up the rest of the function (separate functions is a firewall against side effects).
I like putting comments above paragraphs of code so that you can skim it by trusting the comments, but you can also inspect any individual paragraphs implementation without losing your context.
Agreed. A comment can also explain things that a function name cannot.
I’m all for splitting code into paragraphs with a well written comment above each.
Of course I also extract functions, but only when there are obvious benefit, like removing repetition.
I agree, one of my favorite ide functions is the inline button. Because all too often that supposedly clear name turns out to be not clear at all.
Perhaps it would not be quite as bad if there was a clear distinction between "this is a function for reuse" and "this is a function that's naturally just elaborate code folding", but that does not really exist. Just stick to single-assignment variables and keep their scopes short and enjoy the good parts of sequential code.
> All that takes up more memory when you're reading their code, because due to leaky abstractions the actual implementation of whatever the function name that you replace it with is often important.
"Better names" is a good answer for a sloppy pattern that looks like this:
fun saveRecord(record){
wireRecord = record.toWireRecord();
innerSaveRecord(wireRecord)
}
fun innerSaveRecord(wireRecord){
log("saving record " + wireRecord)
saveWireRecord(wireRecord)
}
That's a stupid level of nesting instead of a reasonably-named "convertAndLogAndSaveRecord()" - then you get into debates about if logging should be in that name, otherwise it's happening despite the name not saying it, etc.
So all that to say that moving into functions alone can be good, or can be bad.
The easiest-to-define benefit of the verbosely-named "convertAndLogAndSaveRecord()" is that it becomes easily testable vs having those lines inlined somewhere. (I'm often much more ambivalent on "reuse" benefits - if your function is specific it can't be made reusable without making it less well defined.)
But as far as I know, the world doesn't offer me a way to test that the function not only does what it says but doesn't do what it doesn't say. And it's similarly hard to talk other programmers out of shoehorning new logic into existing functions - now making them harder to reason about - rather than new functions that are composed differently or with the existing ones.
Because if "checkAccountingProcessResultsAgainstAudit" is going to give you readability benefits, it damn well better do what it says every time you call it instead of becoming a maze of conditionals and optional behavior.
Besides giving a block of code a name, functions also reduce the scope of the data the code can possibly be acting on. They also introduce a namespace that lets symbol names focus on locally relevant information. How did this huge refactoring patch you are describing hold up in code review?
This is my experience too. I love hiding detail for readability, but you have to hide the right details!
It's also definitely related to testability for me. If I'm pulling out the right details, then I'll often get a nice cluster of tests that pin down the higher-level concept in a way where it's both the production and test code that gets more readable.
The problem is those who create the messes to need that advice probably will take that advice and create even bigger messes
In practice, pithy adages don't get us any closer to sanity
Really the only news you can use is
1. Try to modify things with code you didn't write by not simply throwing parts away
2. If you think it's really difficult to deal with, figure out if other people agree with you.
3. understand why everybody thinks this
4. If you are doing that in your own code, then stop doing that.
You have to viscerally understand why a practice is bad and how doing it affects other people.
This is how you can intuitively avoid such practices in the future. Not through things that rhyme or acronyms that spell words but through social intelligence. It's fundamentally behavior
* great ability at naming methods - which isn't very common
* massive discipline at RE-naming methods when they get changed even slightly to do something more
Regarding the latter, I've seen so many times methods which originally described what they were doing, but now they don't any more, that I never just trust the name to tell me what a method really does.
Naming is probably the single hardest part of writing scalable, legacy code. Cache invalidation is a cakewalk in comparison. I believe this is in part because naming is a miniature exercise in empathy for future readers particular of a piece of code.
One of the advantages of functions is that a well-named function is self-documenting. If you can take a bunch of lines and wrap them in a function whose name summarizes exactly what it does, then you have improved readability in my opinion. In this example, I don't really need to know the details of how the query parameters are extracted. I just want to know I've got them.
Usually when I see one-line functions they cost more in short term memory than they save. There's good exceptions to this with something like a horrible conditional that is necessarily horrible so you can bust it out into a method with a doc comment about the horribleness, its history, and its subtle madness. But developers that just break out one-line methods all over the place wind up writing hard to understand code.
Beat me to it. Yes, I ran a quick benchmark[0] and not running a function always wins. now, one may argue that you are running a call, but if the interpreter was smart it would convert the small function calls to just a noops.
Summary
'Perl/nofunc' ran
1.63 ± 0.30 times faster than 'Perl/func'
1.73 ± 0.34 times faster than 'Lua/nofunc'
2.11 ± 0.55 times faster than 'Lua/func'
9.37 ± 1.62 times faster than 'Python/nofunc'
12.18 ± 1.90 times faster than 'TCL/nofunc'
16.02 ± 2.58 times faster than 'TCL/func'
18.64 ± 3.02 times faster than 'Python/func'
> A consonant will be denoted by c, a vowel by v. A list ccc... of length greater than 0 will be denoted by C, and a list vvv... of length greater than 0 will be denoted by V. Any word, or part of a word, therefore has one of the four forms:
CVCV ... C
CVCV ... V
VCVC ... C
VCVC ... V
Those functions get more annoying when debugging though. Because now you have to jump to the body of the function to see if it really does what it says. And you view the body out of context so it is harder to see if there is a wrong assumption between the callee and caller.
On the whole I think such functions are valuable. But they do have downsides.
Genuinely, they ate not annoying to me at all when debugging. I check whether function returned what I expected from its inputs and either look for bug there or move on.
Yeah, but these little 5 line functions with one caller whose only purpose is to avoid a 50 line function somewhere else are almost never named well. At best, I can infer about 20% of what is going on without stepping into all those little helpers.
One issue with functional pipelines is that the reader has to keep track of what the types and the data are on each line. It’s fine for 2-3 lines, but it can get non-obvious quite quickly. Assigning intermediate points to named variables can be appropriate, or indeed factoring portions of the pipeline out into separate functions.
That may be true for less capable editors, but IntelliJ (and therefore Rubymine for the cited code block) annotates the stream type variable when it can prove what it is: https://www.jetbrains.com/help/ruby/viewing-reference-inform... regrettably doesn't show an example of what I'm talking about
That’s nice, but it just highlights that the coding style isn’t sufficiently intelligible by itself. We can’t and shouldn’t rely on a static analyzer to be active to make code intelligible everywhere code is displayed (pull requests, diffs, ...).
Doing so favors writing code over reading code, which is generally the wrong bias. Editor support is appropriate and useful to help writing readable code, but it’s bad for editor support to become a prerequisite for existing code to be comprehensible.
> That’s nice, but it just highlights that the coding style isn’t sufficiently intelligible by itself. We can’t and shouldn’t rely on a static analyzer to be active to make code intelligible everywhere code is displayed (pull requests, diffs, ...).
Yes, we absolutely should rely on that.
Why would programmers, who are basically tool-builders, reject tools that help us write and analyze our programs? This is not the 1970s, and bare-bones text editors are not the only option. Why should we, who build tools for others, restrict ourselves to a self-imposed unreasonable simplicity?
You are free to use Vi or whatever, but don't complain when everyone else uses better tools to improve their productivity, and if the code they write is better suited for those tools that is perfectly fine.
I’m all for IDEs for writing software, but not as a prerequisite for reading source code, because the latter happens in all kinds of different contexts. E.g. a software like GitLab shouldn’t require an integrated static analyzer for any or all programming languages. That would only increase the barrier to entry for all software that happens to display source code. If source code can’t be read and understood as-is anymore, that just impedes interoperability and universality of tooling.
Surely you must understand that the functionality provided by the tools will impact how the code is written, tested, reviewed, and deployed.
There is no ideal world where we can use tools to write code and it would be perfectly readable for people who don't use those tools.
Embrace the modern ecosystem, or build something better, or take a time machine back to the 1970s where there were no IDEs, source control, or CI/CD. Do you really think that would be better?
I disagree, because it reduces the freedom of choice and variety of tools available for software development, and increases the dependency on bespoke tooling. Using a more sophisticated word processor shouldn’t reduce the readability of the text produced. If anything, IDE features should be helping to increase the readability of the resulting source code, instead of promoting a coding style that decreases it.
This is about decoupling. The same way one should be able to freely pick a VCS and CI/CD system of choice, one should also be free to pick the IDE, or any other system concerned with source code, of choice. Those should remain loosely coupled, and should each be easy to replace. The more requirements you impose on how source code has to be marked up for intelligible display, the more you restrict the choice of tools it can usefully work with, and the more laborious it becomes to create new adequate tools in that field.
We should reject tools that only help us understand code that we are writing.
Put differently, we should avoid relying on functionality that isn't present in most places where we need to understand code.
Hence, static analysis should be present in many more places than just the IDE. Merge requests, commit diffs, and perhaps code blocks in discussions are prime targets.
I think this might be one case where the CLI is truly limiting us. No opportunity for contexts menus or pop-ups makes presenting the results of static analysis much harder.
One way I try to solve that problem in Rust: use repeated `let thing = simple.invocation();`. No need to rename the variable `thing` if not neccessary. Rust allows redefining a variable with the same name. Additionally I sometimes give the thing a type which makes it even more explicit.
Something like this:
let thing: Thing = source.prepare();
let thing = thing.make_iterator();
let (first, second) = thing.split();
let first = first.filter();
let second = second.reverse();
let items: Vec<Item> = first.join(second);
Just to give an idea how to make a complicated ritual explicit and simple.
This simplifies debugging too. You can set a breakpoint and view the variables or you can use println!.
This does nothing for when it's a map of string to array of map of string to string. Shape is not meaning, and naming things helps communicate meaning.
I think it is compounded in the article exemple by the flow of the code being non obvious. The map applies to code in its last argument. The lambda is the last action and the url parsing will happen first but is last in the line.
I have far less issue with the piping operator in Ocaml (|>) which works exactly like a shell pipe because the code is in sequential order and that makes a huge difference.
Python like JS weren’t designed as functional languages and it shows in their syntax. Still grateful for the added functionality however.
Definitely a good point, refactoring is great and has a bunch of benefits but can also be overdone and create problems. I've heard/read Sandi Metz talk about this as "The Wrong Abstraction"[1].
The basic argument is that any time you do an extract refactor you're creating a new layer of abstraction that the next reader will have to learn and understand. This can also get worse over time as the abstractions drift away from their original purpose.
The solution she provides is to be okay with a little bit of duplication, then as patterns naturally arise in the codebase you can refactor when you know a few use cases and can clearly define the concept.
Yes! IME (24y and counting in the profession) devs reach too quickly for DRY while neglecting its counterbalancing principle: AHA (Avoid Hasty Abstractions).
I've seen this complaint before and it doesn't really align with my experience of frequently seeing literal duplication of code where no extra abstraction was required to avoid it - just selecting the code in question and factoring it out into a function, which, providing you named it sensibly, would also make the code easier to understand and debug.
I could count on one hand the number of times I've seen code where repeating logic would make more sense than any extra levels of abstraction created to avoid it.
The point isn't to maintain the repeated code forever. The point is to generate the repeated code and (maybe) live with it for a bit until you understand what the right abstraction is and then remove the duplication with the correct abstraction.
I've lost track of the number of times that I've taken badly abstracted code, reintroduced all the duplication, then refactored to remove the duplication with an entirely different abstraction and the code was many times easier to read.
And have you lived in a codebase for many years, where you've never come back to a method that you extracted and realized that you named it wrong? Or the requirements of the code grew and the duplication wasn't really at the level that you picked and you wind up with a method that grows all kinds of horrible options to change its internal behavior?
I've definitely seen cases where existing code was reused for a secondary, seemingly similar feature, unnecessarily complicating it and creating maintenance headaches when business requirements between the two features later diverged, and at best you might be able to claim it would have been better to simply copy & paste the code and modify it first, then look at how to refactor to avoid obvious cases of duplication. But I wouldn't consider that an example of developers trying to stay DRY at all costs.
Honestly the biggest problem I've had with maintaining the principle of not duplicating code is when a PO and/or UI/UX designer think we should only apply improvements to one feature to reduce the amount of work, when in fact it should be less work to apply them to all.
Nim, D, VimScript and other languages have “uniform function call syntax”, which allows you to chain arbitrary functions like this (not just the ones that the author decided to declare as methods, like in Ruby and other class-oriented languages).
I wonder how much of Go's simplicity comes from having "package.function" syntax that doesn't allow you to put too many functions in a single line? People complain about Java because of the length of the names. Do they complain about to many functions in line? Maybe with ( ? : ) Lisp has )))))) which seems like a stupid complaint, but maybe that's tied to people having to remember too much?
import std.algorithm, std.array, std.stdio;
// Print sorted lines of a file.
void main()
{
auto sortedLines = File("file.txt") // Open for reading
.byLineCopy() // Read persistent lines
.array() // into an array
.sort(); // then sort them
foreach (line; sortedLines)
writeln(line);
}
For me that's questionable. Yes, there's code which fits finely with streams and looks very readable. Yet there's code which looks like it was shoehorned into a Procrustean bed and looks much better with ordinary loops.
It's been my feeling for a while that debugger technology hasn't really caught up with newer styles of coding (despite them having been around for over a decade). Only being able to set a breakpoint at a line-level or watch values that are assigned to a named variable is incredibly limiting.
gdb for a long time has been able to watch almost any expression at all. Unfortunately the visual studio debugger can't watch very much at all and that's what many of us are stuck with.
VS's debugger can watch arbitrary expressions, but it's not necessarily what you want (and admittedly isn't always reliable, or causes severe performance issues depending on the language etc.). What I'd really like is the ability to set breakpoints anywhere at all on a line, including a line like `xyz.map(_ -> ...).filter(_ -> ...)` such that, e.g. I can set a breakpoint between map( ) finishing and filter being called such that I can see what the map function has returned/what will be passed to filter, and of course at any point inside the lambda functions.
And it's not just VS (or even VS code), but Chrome DevTools, XCode, IntelliJ/Android Studio etc. etc.
This is already showing why the the Python list/iterator comprehension syntax isn't great. The part "s.partition('?')[-1].split('&')" reads from left to right, and then the rest is read from end to beginning. It gets even more confusing with nested comprehensions. In my opinion both the dotted pipeline style and the Lisp style where you always start from the deepest nesting level are both more readable than Python's approach with sometimes from left to right, sometimes from right to left and sometimes from middle out.
Python's lambdas can have as many lines as you want. Just wrap parens around it. Hissp uses this form as a compilation target. Its REPL shows the Python compilation. Play around with it til you get it: https://github.com/gilch/hissp
While you can have multiple lines, you are not allowed to have multiple statements.
Nobody wants to write code chained together with ternaries and and-operators. When someone says multi-line lambdas, what people actually mean is multi-statement lambdas.
The misleading part wasn't from me. I'm trying to correct a common misconception here. Both "multiple" and "lines" are wrong.
Lambdas can't have single statements either. The body is only an expression. So why are we saying "multiple" (a word to distinguish from "single") if you meant to multiply statements? The number of statements is zero.
Does a try/except fit on one line? That's a try statement. Does a class? Technically possible, but typically no. That's a class statement. It's not just the "class" word, or the first line. The class statement includes its body. Many Python statement types can't fit on one line, and many others don't have to. So why are we saying "lines" to mean statements when these are not remotely the same thing?
The statement/expression distinction is a holdover from FORTRAN. Statements are just expressions that don't return a value. (Yes, Smalltalk and Haskell can implement control structures with expressions just fine, thank you.) Many functional languages do away with the distinction altogether and just use expressions. You only really need lambdas in Python for the functional style, and in functional style, expressions are all you need!
No, this pipeline is not more readable. It is a annoying to have to constantly have to read it. It makes it harder to figure out what larger algorithm and design is.
Reading this literally makes me sick to my stomach. Language design is much more important than language popularity, although it will be popularity that wins. (Yay downvotes for pointing out things everyone can see - highschool dynamics)
Just because it’s a different syntax than you’re used to reading in another language doesn’t make it ugly. If you’re used to reading it and work in the language regularly, it actually looks quite clean.
This sounds like a Windows user who can’t stand macOS because they don’t know where anything is.
Your post downvote edit assumes your opinion here is objective. It isn’t.
collect: [ :keyAndValue | (keyAndValue splitAt: '=') last ].
is why.
EDIT: (it's been a few months since I used it, so I) made a mistake in the syntax (declared local variable instead of a formal block argument), it's fixed now.
Posting like this will get you banned on HN, regardless of how right you are or feel you are. Please review the site guidelines (https://news.ycombinator.com/newsguidelines.html) and please don't post like this to HN again.
This is particularly important when your argument happens to be a good one, because in that case you're discrediting the truth by posting abusively (see https://hn.algolia.com/?dateRange=all&page=0&prefix=true&sor... for past explanations of this point). That hurts everyone. It particularly hurts those of us who happen to agree with your view (me, for example—I haven't read much of this thread but I'm pretty sure I've made similar arguments about readability in the past).
I don’t know Russian. Your point doesn’t line up with the topic at all. I know Ruby. I still think it’s unreadable. Can you try next time?
I feel incredibly sorry for the poor kids you teach. They will be suffering aftereffects for the rest of their lives.
I feel bad that your account hasn’t been flagged for your behaviour on here. For someone who thinks they understand Ruby so well, you certainly haven’t taken any time to understand the rules. Personal attacks? Really? You just showed your cards. Weak arguments are followed with weak ad hominem.
It’s not just me, no one is teaching Ruby to kids because it’s not a good language for readability. Take it up with all of the teachers who see it just as I do.
Now I know where Rust got some of its syntax from...
As an aside, when I see samples like this, it makes me itchy. I hope and assume that they're being used as made-up snippets just to illustrate a point, and aren't being lifted from an actual codebase.
Because... ugh... isn't it obvious? Attacker-controlled input such as URLs should never be manipulated with naive string processing! Always use a proper parsing library. Not to mention that complexities of URL encoding, character escapes, etc...
The problem is that the author is using abstractions at the wrong level, with or without his fixes. The correct solution would be something like:
var uri = new Uri( "http://foo/demo?test=a&blah=b%20c" );
var map = System.Web.HttpUtility.ParseQueryString( uri.Query );
Console.Out.WriteLine( "is blah equal to 'b c'?\n{0}", map["blah"] == "b c" );
The above example is C#, but similar code can be written in any language. It's simple, direct, and doesn't violate the "rule of six". It can be read like English:
1. Construct a URI from a given string.
2. Parse the query part of the URI into a map.
3. Test if the 'blah' value in the query is "b c" as expected, with the escaped space decoded properly.
The example of how to apply the "MORF" rule in the article still has low-level operations involved, which doesn't make the code more readable. It doesn't describe the intent, which is the key thing to writing code that doesn't need comments every second line.
What the author is missing is that easy to read/reason/understand about is within the context of making a change to the code to fix a bug, add a feature or make some non-functional improvement to it.
This is what most of the "easy to read" articles forget.
Show me why it is easier to fix a bug, add a feature or make a non-functional improvement to the code with their style than without.
For example, if you've extracted something into its own function, are you then sharing this function and using it in other places as well? If you then change the body of that function, are you now possibly breaking other parts of the code that relied on its old behavior?
If you've introduced a local mutable variable in between two lines, are you then mutating that variable prior/later? Is the query_params different at the end of the function then in the middle? Can you safely use it again?
How easily can you now introduce new behavior before, in the middle, after, and anywhere in-between?
When you modify the behavior to fix a bug, add a feature or make a non-functional improvement, is it an isolated change? How many tests break? Did it require major refactoring to make or very few things had to change? How easy was it to add a test for your new behavior? Was it easy to find the most appropriate place in the code to make the change? Etc.
Sure sometimes maybe you just read code for the fun of understanding what it does, but almost always in practice when you're working on a code base, you only care to understand and reason about the code because you're looking to deliver that next sprint task that involves changing something about it.
I wish more people focused on "easy to change/modify" then simply on "easy to read/understand".
> For example, if you've extracted something into its own function, are you then sharing this function and using it in other places as well? If you then change the body of that function, are you now possibly breaking other parts of the code that relied on its old behavior?
You absolutely are. Which is why I think that DRY was pushed too hard, and for the wrong reasons. Everyone said "you only have to change it once!" rather than "consider how many callers depend on this". There's a reason the "rule of 3" came around; and those reasons I've found were mostly experience and empirically based.
> If you are unit testing you should not have to worry about tweaking a function and it breaking everywhere else.
"should" is a word loaded with authority.
Why?
If you believe a unit tests is for turning an impure function into a pure function (so you can just test what it's doing and no other effects), then in many cases tweaking will break existing unit tests. If the function exists, it's assumed it's used by more than the tests for it. Changing the signature or even the internal dependencies necessarily breaks the known contracts with other units.
which either forces you to fix those contracts - implying you have to go understand those contracts too. Or, you un-abstractify the function by keeping the original, and add a copy with the modifications you needed for the new feature/fix. This keeps the original contracts intact, but you add "bloat" to code - a form of tech-debt.
This is a demonstration of why a code change may seem simple in theory, but when you actually do it, you can see all the side effects that you didn't consider when estimating. This is one of the many ways that estimation without looking at existing code, can be completely off...not to say it will be right-on when estimating and looking at code. I have worked a company with 8 hour planning, where every single change is by committee while looking at existing code. Mercifully, this methodology didn't last more than a few months.
> even the internal dependencies necessarily breaks the known contracts with other units
The implication is that there are unit tests for those dependencies as well. Changing the signature used for them, breaks the same contracts as a wrapping function signature change. Ofc, you may already have tests to cover alternate signatures (overloading, et al) but conceptually it's the same pitfall.
Unless the test cases are incomplete. Or the CI jobs are configured to run tests independently and off-cadence so that code that would break is tested after a merge. Or the tests have a bug in them. And so on.
My opinion is that maintainable code is written first for reading by humans and second for executing by computers.
Unless I'm writing throwaway prototype code (famous last words, lol), I try to write code such that I will be able to figure out what my intention was 6-18 months from now when I'm staring at a piece of code in a panic trying to debug a production issue.
That doesn't mean I'm going to get it right when I write this code. Instead, I'll be able to better ascertain what my assumptions were, how they fell apart in practice, and what a minimal, correct fix that doesn't make things worse might be.
Edit: Incidentally, this also applies to my commit messages. I’m writing them primarily for my future self so that I can figure out WHY I made a change, not WHAT the change was.
My opinion is that you can write code that's easy to understand, and it is also good for the computer to run.
One thing is not in contradiction with the other.
It could lower the reusability of the code, by not having many abstractions, but it will be easy to understand, concise, and it will do what it was written for very well.
That complete disregard for performance is what gave us the endless Electron clients eating CPU/RAM just to do basic chat and music playing stuff.
If the code is written with care for both things, readability and speed, the actual state of things would not be so wasteful. You don't need to make it difficult to understand. Just don't disregard performance as an afterthought.
Nah, I stand on the shoulders of generations of developers, just like everyone else here.
I didn’t claim my opinion was novel, just that it’s mine. I hope others share my opinion, because I’d find codebases that fit my criteria easier to maintain than many other types.
Also, do you agree or disagree with any of the ideas I put forth?
Which original authors am I unintentionally plagiarizing? I’m not usually that adept at quoting verbatim, especially when I’ve completely forgotten who I’m quoting.
Ahh, I apologize for not thoroughly reading the entire comment thread before posting mine. Otherwise I would've upvoted you and said "+1" or something to that effect. I certainly didn't mean to hurt your feelings.
And also thanks for jogging my memory on the source. SICP remains one of my favorite textbooks from college, although it's been about 20 years and so everything's fuzzier than it used to be.
We break everything down and then we reach one of the most difficult problems in software engineering: Coming up with good and short names for all these extra intermediate variables and functions.
Yes, and a source file littered with tiny helper functions that do very specific things and don't make any sense except in the precise context in which they get called, isn't necessarily more readable.
Here, "query_params" means "extract the last three query parameters, raw (i.e. not unescaped and not broken into key-value pairs)." The transformation shown makes precisely nothing more readable or easy to understand. "The second argument to map()" is just as easy for your brain to group into a black box to be analyzed later as a call to an opaque "query_params" function that you need to read the implementation of to really understand what the code is actually doing.
Of course sometimes it's the best solution to just extract local helper functions, especially if the actual function just becomes too unwieldy and/or the helpers are called from more than one place, but in general I try to extract things that do something more general than the thing I'm extracting it from and have an interface / a purpose that's easy to understand and describe on its own.
To stay with the example, actually extracting the query parameters would be a generic, extractable utility. Half-extracting the last three parameters because the function I'm writing needs precisely that for some reason, is a local helper function, and I'd only extract it if there's a good reason, certainly not to make an already trivial function no easier to read.
I wouldn't attack the example too much, it does seem a little contrived to make the point - the very thing I don't like about contriving examples!
It is hard to know whether the principle is valuable with such a weird example. In this example there are lots of other ways it could have been done more meaningfully but, again, don't know if the example is real.
What doesn't generalize is that the example doesn't necessarily stand for a simple one-liner, but as an example it has to be trivial, of course.
But everything else generalizes. I took "splitting out the last 3 params" to mean "doing something very specific to the problem at hand" and extracting that isn't _always_ better.
Your mileage clearly varies, but I found the transformed example much easier to understand. While I suspected it was parsing a query string from the initial code, having that stated explicitly in the variable removed the guessing. I think the main problem is he just didn't go far enough, there was still more to deconstruct.
I suppose the function to parse the query string could have been better, its name isn't very descriptive, and the method with which it parsed wasn't very obvious either (I'd expect to get back a dict or a list of key/value tuples, not a list of strings)
I know a lot of programmers are against comments, but I also think this is exactly the kind of code where a comment is handy..., the purpose of the [-3:] part wasn't obvious to me at all.
For me that's the issue with this whole example; it kinda misses the woods for the trees.
# not interesting (also, use stdlib instead)
def extract_url_params(url: str) -> dict[str, str]:
params = url.split('?')[1].split('&')
return = dict(param.split('=') for param in params)
# This is the thing that needs explaining!
final_3_param_values = list(extract_url_params(url).values())[-3:]
By decomposing the problem in a more natural way, you can entirely direct the reader's attention to the thing that's causing the dissonance. For some reason it's just the the final 3 values they want (both weird requirements). Make that clear from the code!
And yet, I know python and have 15 years of professional experience coding, and (in the limited time I wanted to spend on it), I still don't exactly know what he was trying to accomplish. So congrats if you got it, but I'm sticking with my claim that it's fairly non-obvious. I understand that it's pulling out the last three elements. I have no idea why it's pulling out the last three elements, what purpose that serves and why it's desirable. If those elements were placed in a well named variable, I suspect it'd give a hint as to why the code is doing that. Alas.
You don't really need short names. I wouldn't advocate going full Java naming but trying to compress names just to save a bit of typing is unnecessary. Your IDE will help you out. Just learn to press tab when you've entered enough of the name instead of typing the whole thing.
Shorter names aren't easier to read if you have several similar names used in close proximity. More characters can reduce the congruence and make it easier to differtiate between names.
I agree about fitting things onto one line though. It's usually better to break things up into to several short one line steps rather than have a long multi line statement.
More characters can make it harder to differentiate names. The longer the name is, the harder it is to see it as a single unit while reading. Also, if the long names cause many extra lines, it becomes easier to lose context from the extra distance one moves down the document while reading.
A special case is when you're working on something mathematical. The articles and textbooks that describe the algorithm are all using single characters do represent things, and it is just additional mental load when you read the code and algorithm description together and have to translate between descriptive names and the paper's terminology all the time.
Typically, relatively unspecific names like "i" or "size" are good enough. It's better than not naming at all and producing a complicated expression tree instead. More specific names cost energy, both inventing and reading them (because they are typically longer). Err on the side of short and not too specific.
It depends. Go spelunking through old Unix and Gnu code from the 80s and you'll see a lot of maddening usage of single and double letter variables all over the place where a descriptive name would make things much more readable.
in most situations, I would rather see a complicated statement split over several lines than several simple statements with vague/unhelpful variable names. if the variable name itself doesn't help me understand what it means, I have to remember the full expression anyway.
It takes some deliberate practice and being able to create decent contexts within your code.
It's not a trivial problem but it's not a hard one. It's just that most people don't even try to dedicate a sliver of active brain power to the task because they don't deem it worth it even if they claim to agree on the importance of readability.
TFA missed the point of splitting complex expressions into separate lines: naming the single-use vars clearly makes the whole calculation easy to follow. In the example given, nothing about the one-line `map split over split of split` tells a reader that it's parsing a query string – just splitting it in two and naming the temp var `query_params` makes it clear.
Although `last_3_query_params` would be more precise, and something that explains why TF you'd want that would be better... ツ
React makes it funner. [score, setScore] = useState … means you gotta think of another name for any intermediate “score”-like sub calculations in all the scope levels of the functional component. (Or yuk keep track of the scope you are in mentally). Then throw the firebase api on top of that with it’s myriad intermediate step api and fun fun fun.
I have pretty mixed feelings about this. Personally I find it much easier to debug code that:
1) fits entirely on my screen and
2) doesn't involve much state modification
Every intermediate variable is a chance for me to miss some modification (e.g. it was passed to a func that modifies its arguments) and consequently misunderstand what is happening.
I've been experimenting in Python with the function chaining style of coding enabled by the toolz library. So while not at all idiomatic, the example in the original article would come out as something like this:
I would say that function-chaining this example would constitute over-engineering, but I have found that writing in this style has really helped me express pretty complex function composition in a way that is still concise without using a bunch of intermediate variables.
This particular case is special because it uses 100% library functions. Typically you're composing your own functions, so you just... put breakpoints in your functions.
If you want logging, I've added an example of how to auto-log the composed functions to the gist.
This really resonates with me. I remember when I started programming (at like 10 or so), my dad tried to teach me Smalltalk. Smalltalk is a great language, but there were just too many concepts and abstractions happening on each line of code. To understand even basic code required understanding messages, objects, classes, blocks, etc. Maybe to an 18 year old that would have been ok, but for my 10 year old brain it was too much.
A few months later though, I started with QBASIC. BASIC of course gets an awful rap, but it was so much more intuitive for me at the time. I started out with just global variables and GOTO's everywhere. Over time, I worked up to loops, and subroutines, etc. etc. However, the simplicity of "program runs one line at a time, each line does something obvious" was incredibly important to beginner-me.
Even once I moved to C, when I was an amateur I still had a tendency towards one line per thing happening. I really hated code like
while(i++ < 10) { doSomethingWith(i); }
(Actually, I still do).
As I got more sophisticated in my 20s, I started packing a lot more ideas into a single line. If I'm being perfectly honest, I think some of it was just showing off. You certainly look clever if you can put 3 list comprehensions on one line or use some of the more advanced collections apis. However, besides understandability, I found that style of code had two really big problems:
1) It's a lot harder to debug. Either you can't get a breakpoint in the precise place you want, or you can't insert a print statement easily into a complex expression, or iteration variables become implicit and you lose context.
2) It's hard to add error handling to that type of code. When a lot of things happen in a complex expression, you're depending on the entire expression working.
Luckily I've grown out of that phase, although ironically now my much more mature code looks a lot like the very simplistic code I wrote as a teenager.
3) It's also harder to edit with an editor (like vim), and (IMO) harder to read. I never even do "int x, y = 3;". I always put each variable declaration on its own line.
You know this, but for those unaware, in the previous example, x is not initialized to 3. Similarly, in "int* p1, p2;", p2 is an int, not an int*. Easy to misread.
Another useful rule is to think it terms of intentions and split to those individual intentions. You can always reduce code down to a single function call, but was that the original intention? Try to think as a reader that just stumbled on it without any other context. `result = DoEverything(payload)` is often less readable than `step1Result = DoImportantStep1(payload); finalResult = DoImportantStep2(step1Result);` if Step1 and Step2 mirror the actual process that goes in your mind when solving that particular problem, so when re-visiting you can understand what's going on faster, without having to visit the implementation of the single `DoEverything` function.
Edit: To clarify a bit more, in contrast to the rule of six, i'd definitely keep a line that is more complex than usual but conveys the intention of my thinking, rather than splitting it to multiple lines and losing that important information, losing the original intention.
“Rule of six” is generally interesting - I came upon the concept when reading the book “Nightfall” by Isaac Asimov as a kid. There’s a line in the book about the number of stars in the sky, and how people can’t really grasp numbers more than 5-10. It got me thinking about trying to visualize a set of 3, 4, or 5 distinct objects without splitting them into groups. I genuinely can’t do it for more than 5 or 6 of something.
I also remember reading about a study where chess masters and non-experts were asked to memorize chess boards. Average people could only remember 5-7 piece locations where chess masters could remember the entire board. But when the piece layout was random (rather than from real chess matches) the experts weren’t much better than the non-experts. It’s speaks to the abstractions our brain creates to deal with limited working memory.
That cumbersome line of python is a good example. As an experienced python person, I immediately found myself giving names to the chunks to understand it.
Overall very good advice. Your code should explain the steps it takes to solve a problem (or in a more functional language, explain the solution), not be as terse and clever as possible. Keystrokes are cheap; thinking is expensive.
It’s funny because what I found confusing initially reading the code is the behaviour of split and I still do after the article. I know see that this is because the article uses a magic value and magic values are the bane of readability.
See, the first split use made me think it always returned the left and right part after splitting at the first match - 0 being left and 1 right. This is not the case. The code implicitly relies on this being a url.
But then the second split is accessed with the weird [-3:] which I have to assume to mean the last 3 elements. I assumed then that split must return a list but started wondering: why 3 elements only? I still don’t know. I wasn’t helped by the single letter named variables either.
I think people might want to focus on the basics before venturing into grand consideration about splitting lines and putting code in function. The one liner with proper names is too long but understandable:
If you are going for human readability then making your code expressive is the only way. Abstract away the code parts under a layer of very simply named functions/classes and boom! even a child will be able to understand what's going on.
Obviously, that isn't always possible. I find this approach especially useful in writing e2e browser tests. You write an abstraction over the testing framework's (playwright, puppeteer etc) interaction and then use that in your tests.
So instead of writing:
await page.click(".play-button");
You do:
await app.play();
This also has the benefit of extreme reusability. Doesn't work for everything though.
I like how this article explains that "clean" must be "readable for humans". However, the concerns raised are only superficial. It's much more important to get the larger scale structure right. I recommend drawing diagrams and explaining the architecture to humans. Then again, I'm not saying overdo it, because some things are hard to draw, some are hard to explain. In the end, it's important to get a complete understanding of a certain module, and the code should then be relatively easy to write.
I have learned to take a step back when I find myself having a hard time to get the code "clean". Often I put that thing to rest if possible, maybe for days, months, or even years. There could be a simple solution that solves 80% of the problem, and that can ease the pressure coming from the stakeholders. If it kind-of-works and can be produced in a short time, that is much better than going down a rabbit hole for months, coming out at the other side (probably burnt out) with a solution you can't deploy because it's too complicated.
> Show me your flowcharts (code) and conceal your tables (data structures), and I shall continue to be mystified. Show me your tables (data structures), and I won’t usually need your flowcharts (code); they’ll be obvious.
-- Fred Brooks, The Mythical Man-Month, 1975
> a computer language is not just a way of getting a computer to perform operations but rather that it is a novel formal medium for expressing ideas about methodology. Thus, programs must be written for people to read and only incidentally for machines to execute.
-- Abelson & Sussman, The Structure and Interpretation of Computer Programs, 1984
Most pithy aphorisms are over-simplifications. "Want cleaner code? Use the rule of six" probably is too. But they can still give you a useful way of thinking about a problem that you might have forgotten to consider, and can give you a Platonic ideal as a useful guiding star even if you know you'll never reach that sort of purity in the real world.
> The first quote is an over-simplification that often does not hold in practice: one usually needs both. The exceptions are mostly trivial programs.
Depends, but not on the complexity of the application.
Given the data design of even the most complex set of applications that all interact with the same data, you can almost always predict what business logic is supported by the set of applications.
Given all the code for the same set of applications, and you'd find it difficult, if not impossible, to predict what the business logic is across all the applications.
> Given the data design of even the most complex set of applications that all interact with the same data, you can almost always predict what business logic is supported by the set of applications.
How so? Every table, file, or other data structure added increases the possible of uses with factorial complexity (think of, e.g., the traveling salesman or other graph/network problems). Without seeing something else, e.g., the code, a flowchart, or other diagrams it is impossible to predict what the program actually does. One can guess at bits and pieces of it, but not more.
It’s like saying that given a table of numbers one can predict exactly what functions produced them.
> How so? Every table, file, or other data structure added increases the possible of uses with factorial complexity (think of, e.g., the traveling salesman or other graph/network problems).
Only if they're randomly named.
> Without seeing something else, e.g., the code, a flowchart, or other diagrams it is impossible to predict what the program actually does.
Seeing a table named "invoices", which has a column named "user_id" that is a foreign key for table "user" doesn't give you at least a few clues about the business logic there?
Seriously, I want to see an example (choose an open source project) of a project where the data structures don't give any clue to what the business logic is.
Once you go looking for that example, you'll see why the data structures are more revealing than looking at the code.
It gives clues, but it doesn't tell the whole story. It certainly isn't enough to do anything other than make a pure guess at what the application actually does and more importantly how it does it.
> It gives clues, but it doesn't tell the whole story. It certainly isn't enough to do anything other than make a pure guess at what the application actually does and more importantly how it does it.
I'm not claiming that it will tell the whole story; I am claiming that it tells so much more of the story than the code, that you may as well just not look at the code if you have the data structures.
OTOH, if you look at only the code and not the data structures, you'll have so little of the story that you cannot make any change without risk of breaking some other piece of logic.
There's just no comparison between "The code tells us what the applications should do" and "The data tells us what the applications should do".
If you can find any examples where it is faster to determine what business logic is supported by the application by examining code (as opposed to being faster to determine what business logic is supported by examining the data structures), I'd very much like to see that.
I tend to find that top-down designs usually end up clunky. In my experience, bottom up designs (starting with specific things and creating new abstractions as they're needed) tends to create simpler and more obvious designs. You also don't waste time on hypotheticals, since every line you write has a purpose. This blog post really nails it: https://caseymuratori.com/blog_0015
In that context, I don't think this is superficial at all. If your code is hard to read, I suspect your design is hard to read too.
Sure, for super important architecture decisions you have to make a few top-down (spatial partitioning structures, database decisions, network architecture, etc.), but I think it's generally better to late-bind on those decisions if you can.
It is superficial. No amount of trying to pretty up the code can fix underlying design deficiencies. That's what I said, so we're not even disagreeing here :)
I know the semantic compression post, and I don't think it makes a point for bottom-up design. I find top-down and bottom-up to be quite misleading anyway. Someone told me, they don't like to think of things at the "top" and the "bottom". It's data transformations, maybe more like "left to right".
If you design bottom-up, you end up with lots of artifacts you never needed (and likely still missing the ones you can make use of). If you design top-down, you end up with lots of code you don't need. (this is where semantic compression comes in, in my understanding).
I suspect that if you like to think bottom-up, maybe that's because you like it more at the bottom (you are a low-level type of guy, or like to make libraries). If you like to think top-down, maybe you like it more at the top.
I like the semantic compression term because it reduces the act of design to the essentials, without introducing fluff terms or opinions. I find myself doing this compression no matter what kind of code I'm writing.
Well, you might know of that blog post, but might want to reread it, I don't think it says what you think it says. The example he steps through is very clearly bottom up. He even outright says it:
> This is a very bottom-up programming methodology, a pseudo-variant of which has recently gained the monicker “refactoring”, even though that is a ridiculous term for a number of reasons that are not worth belaboring at the moment. ...
> Like a good compressor, I don’t reuse anything until I have at least two instances of it occurring. Many programmers don’t understand how important this is, and try to write “reusable” code right off the bat, but that is probably one of the biggest mistakes you can make. My mantra is, “make your code usable before you try to make it reusable”.
> I always begin by just typing out exactly what I want to happen in each specific case, without any regard to “correctness” or “abstraction” or any other buzzword, and I get that working. Then, when I find myself doing the same thing a second time somewhere else, that is when I pull out the reusable portion and share it, effectively “compressing” the code. I like “compress” better as an analogy, because it means something useful, as opposed to the often-used “abstracting”, which doesn’t really imply anything useful. Who cares if code is abstract?
And I don't understand how you'd end up with artifacts you don't need with that approach. Do you mean intermediate steps you refactor? In that case I suppose so, but that's an important part of the process, not a waste. You're never going to get the initial design exactly perfect so you need to iterate on it.
What he means there by saying "bottom-up", as far as I can tell, is that you "compress as you go", i.e. it is bottom-up compression, in a similar way to how you can also write bottom-up parsers.
It's not stating that you should start by creating lots of little artifacts before you have a great plan (bottom-up design) nor that you should start implementing the "non-functional requirements" (another confusing term) on a high level before you know the platform and other functional requirements, which is top-down design.
So that's perhaps a justification why I said earlier that these terms are misleading :-)
As to the other thing that I said, that bottom-up tends to create things you don't actually need, it's almost immediately following the definition of bottom-up. You can even find this on the Wikipedia page, which states that in practice, software-development typically uses a combination of bottom-up and top-down for that reason.
(In other words, always strive to know as much as possible about requirements both at the "top" as well as at the "bottom", as early as possible. Or the risk is that the design won't fit some part of the requirements and large parts need to be redone).
I still don't see the connection with what I originally wrote, in any case.
> It's not stating that you should start by creating lots of little artifacts before you have a great plan (bottom-up design)
Look, you can disagree with his conclusion, but that is exactly what he's saying. The post is long but it's very clear and methodical about how he's suggesting code should be designed and written. I happen to agree with him.
From the post: "if the reusable code is already suitable, you just use it, but if it’s not, you decide whether or not you should modify how it works, or whether you should introduce a new layer on top of or underneath it."
I'm having a really hard time interpreting this as "you should design / program bottom-up".
If you go to Wikipedia, bottom-up design is roughly defined as taking the existing stuff and building new things on top. I still fail to see where the compression part comes in here.
Neither of the two directions will look back at the mess they've made and compress it. They're just ways to develop the system by adding to it incrementally.
But then, these terms are generally poorly understood (as evident from various discussions I've had) and I don't want to claim to have the perfect understanding either. Nor do I want to be "Look"'ed any more. So, whatever.
Honestly mate, I just don't really understand the point you're trying to make. Originally it was that it doesn't matter if the line-by-line code is understandable because the high level design is what's important. I think the line by line clarity is important, because if that's confusing in my experience the design tends to be muddled also. But then you have these definitions of bottom up and top down that are very confusing and seem to exist mostly to you. I'll simplify what my definition is: bottom up programming means you write the code without any abstractions first, and then you generate the abstractions as you find commonalities and repeated patterns. I find this to be superior to the other approach, of generating abstractions first and then filling in the code. That is my opinion, I know other's believe differently. And "look" is not an insult.
If all the code in your project was written like a), how would you feel? Does it make your job easier or harder?
I'll tell you how most people feel when they read code that looks like a):
- The author didn't care about other maintainers.
- The author is selfish and does not have empathy for others.
- The author ruined my fucking day.
- Team members are competing by sabotaging each other's productivity.
- If I clean this, by the time I am done, the author would have pushed 10 more commits that look exactly like this and eventually become my boss.
- The author is wasting everyone's time.
- The author is forcing others to volunteer to clean up after them.
- Why does management tolerate code following the a) style? A simple intervention would make it go away and my job would be so much better.
- It's sad that everyone is too busy looking at Jira and nobody cares about the actual fucking product.
- This is slowing everyone down and I have stuff to do.
- The code is error prone, one day I'll break it.
- Why should I contribute quality code if low quality is acceptable?
As you can see, it objectively fucking sucks. It's draining, demoralizing to read, it's frustrating, it wastes people's time, it gives people the perception that nobody fucking cares and the code is everyone's toilet with no trip lever.
And while it's "superficial", it's the surface that all engineers interact with. If I spread superglue over the surface of your kitchen counter and every dish and utensil in your kitchen every day around lunch time, that problem will also be "superficial", but it will ruin your life.
So, the conclusion is: Just fucking write clean code. Shitty code ruins the morale of people who care, who are the people that want to build great things not the ones cashing a paycheck and resting and vesting.
You are not a full-time architect, you are not in business development/marketing/finance or whatever, you are in the fucking engineering department. Your contribution to the business are your deliverables. The "superficial" stuff you talk about is your job. Do it.
"Ah ah ah, you didn't say the magic word!! ah ah ah!" Don't be the fucking Dennis Nedry of the team. Format your code, make it readable by your team and your future self.
Do you want everyone to love you? Write code like this:
Yes, I sometimes do that as well to signal a part that's non-obvious or easy to overlook. Ideally with a little comment for the reason.
Same with my (admittedly rather dirty) openSCAD designs, where I like to explicitly add tolerances to the measurements while keeping them separate, e.g. `width = 34 + 0.25;`. Especially because tolerances often have to be tweaked a couple times.
The parent article was about replacing dense code with more lines of simpler syntax. I assumed you were in the shorter-is-better camp and willing to prove it with your example, where shorter undoubtedly is better.
I found your example far stretched, and designed another case using your material (arithmetic operations) but applying only 2 repetitions, much in line with the spirit of the fine article.
I hope I managed to show that, although we can agree that "very very longer is worse", this not always implies "shorter is better".
This is why setting an arbitrarily short max line length matters. And consequently why auto-formatters suck.
A short line length, while yes imperfect, forces complex lines to be decomposed into individual concepts. And it allows the code to read like a book rather than <there is literally no other media format that you read sideways>. Ultra-wide monitors be damned.
And auto-formatters suck because they don’t split concepts onto individual lines. They can’t. They just mangle code and scrunch it into whatever space is allowed without regard to how the code reads. The idea of them is great and intensely alluring, but the implementation leaves much to be desired. If an auto-formatter could make my code look and read like a LaTeX document, I’d shut up already.
So if you want people to implicitly start structuring their code as advised in this post, set a 80 or 100 char line length. And adopt a fuzzy “one statement per line” philosophy.
Agreed, auto-formatters have the single purpose of making code on screen visually more readable for humans, but seem to not consider that humans are not machines.
There is no regard for "visual code density", no attempt to use vertical alignment to highlight similarities and differences between consecutive lines.
To be fair considering such visual details is a complex task and probably hell to implement.
This seems perfectly reasonable advice. However I do wonder how many people actually struggle with this sort of code quality. It's certainly more than a few, since I've encountered bad code with these issues. But it's not exactly the most pressing issue either. As the author demonstrated, you can refactor this with a little thought. It's the code equivalent of tidying your room, sweeping the floors and putting your stuff away.
Whereas the refactoring issues I'd love to learn more about are the equivalent of a sinkhole in your living room. Stuff like "you have data dependencies that go in, out, left, right, and through the code", or "the codebase is a mishmash of React combined with Vanilla JS that is hooked up to a custom PHP MVC". Basically refactoring that involves issues that cannot be cleaned up all at once, that involve deep architectural decisions and that require some amount of buy-in from the team.
Mostly I'd like to know more about this because I've realized that I'm not very good at it. My inclination is to just refactor everything and that's not a feasible strategy. I also struggle to balance it with getting feature work done. Definitely something I plan on reading more about.
My opinion is that each line of code should be easily understandable. Without that, code is very hard to work with.
You're right that other code problems can be worse. But that's no excuse to avoid doing the basics.
To clean up system design issues, you must first know what a better system design would be. It's not enough to realize that what you have is bad.
I do this a lot, and part of my approach is to always be incremental. Improve one detail/aspect at a time. The worst, very tempting, idea in this field is to throw everything away and start over...
> I also struggle to balance it with getting feature work done
FWIW, I like to spend 1/3 of my time cleaning up and refactoring.
I’ve seen some engineers that think it’s clever to put everything into a one-line list comprehension where possible, even if that means rewriting named variables as letters to make them fit. The result is really hard to read.
I’ve also (more common) encountered engineers that don’t actively try to be clever by being terse, but also don’t put their mind to writing clearly.
Put differently, I think one has to actively try to write easy-to-read code.
I agree with your point that this sort of micro-style point isn’t as big as architectural questions, but it’s definitely something you want to teach junior engineers so that it’s second nature by the time they are at the level where they are thinking about architecture.
For that you can try reading Bob Martin, Martin Fowler, Kent Beck, Domain Driven Design, Hexagonal, etc. - but you also just need to build for a decade while thinking about that stuff to really master it. Sadly architecture often seems more craft than formal engineering at this level.
Exactly. This post is helpful to beginners, sure.... but after some experience, the problem you describe becomes much more pressing.
I still don't have a good solution other than try to "keep things simple" from the beginning, then as soon as new features are introduced and everything becomes messy, mercilessly refactor the architecture itself to make things "make sense" again. I fully realize that this is not very helpful because what does "make sense" even mean in a code base? But that's the best I can come up with and I don't think there's anything more specific that can be said :(
What always helps me with architecture redesign projects like that is trying to define a goal to work towards: getting alway from that custom PHP MVC and to a proper Symfony application, for example. If you know where you want to go, it’s easier to stay focused and to align what you do with what you want to achieve.
I see a troubling trend with some coworkers where they seem to stretch the limits of time and space to make every line as dense as possible, usually using lodash. I think it is a point of pride for them, but I think it's obvious that everyone's life would be easier if they just wrote their code out "long form" and, god willing, added some comments for various steps. Instead, I find myself having to re-write ultra-dense blobs of code in order to debug or even simply understand what's going on.
Once I read something along the lines of “every programmer goes through that phase were we wants to show how clever he is, by writing whole programs in one line. Until he understands how stupid that is”. I do not have the source, regrettably.
I was taking my first multi-threaded resource allocation course when I first ran into the famous Kernighan quote.
> “Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.”
It clicked and instantly disabused me of the notion that smart people write code that's any smarter than the minimum required to solve the problem at hand.
Kernighan wrongly assumed that to do something twice as hard, you need someone twice as clever. Most probably you need someone just as clever and twice the time.
Fair. In my practical experience, it's 50/50 2x clever vs 2x time.
Mostly down to task decomposition.
Some of the stuff in compiler debugging? Just 2x clever, because you have to hold it all in your head at once to solve it.
Circuit debugging or data transformation debugging? You can break it down into smaller pieces, then methodically (and laboriously) worth through those pieces.
I think there's a real smell with those long, dense lines of code. Tends to mean your data structures are out of control: objects with arrays that point to other objects that then also have arrays on them. My oh my.
Comments being required are also another smell that the code doesn't explain itself. I know this is said so often it's a cliche, but it really is true.
I think both of these things point back to the same problem: out of control data structures.
Sure, if the computer can figure out what a given fragment of code is supposed to do, so can you (a sufficiently clever programmer). The question rather is, do you spend 20s reading a comment or 15m to solve the riddle?
There's a real danger that comments aren't updated when code is, particularly if 3rd parties make those changes. This is one of the corners where there will never be a single answer which is right in all circumstances.
> There's a real danger that comments aren't updated when code is, particularly if 3rd parties make those changes.
So much this. If it took you everything you learned over the last week + an epiphany to come up with a bit of code, how is someone scanning through supposed to understand it?
Do your co-workers use lodash's chain functionality? I've found it useful to achieve kouteiheika's ideal from the current top comment, with top to bottom readability without too much mental overhead.
> I see a troubling trend with some coworkers where they seem to stretch the limits of time and space to make every line as dense as possible, usually using lodash. I think it is a point of pride for them, but I think it's obvious that everyone's life would be easier if they just wrote their code out "long form"
Concision can be used for emphasis as verbosity can be used to obscure.
> Instead, I find myself having to re-write ultra-dense blobs of code in order to debug or even simply understand what's going on.
Is this problem because their method is inherently nee complex or is it due to lack of familiarity?
Perhaps they debug that code differently then you do and it's incompatible with your previous mental model?
It is not new, it’s been ages since some developers try to show off by bringing “cool” one liners to solve problems. Stretching operators, bringing up imaginative uses for lambda expressions or kind of abusing parts of the language to make some other teammate or reviewer, What did you make there?
I believe, definitely, that they are quite intelligent people that know a lot about the language or maths, but definitely they are not usually making the smartest choice, because you should use “languages” in order for the people to understand you.
In the last times I’ve been seeing lots of “show off” with nitty gritty features of C++… lambdas, templates and inheritance in a pattern that reminds me to the characters in an Agatha Christie book, where you need a graph to keep up with it…
Hope this era ends soon.
I had a professor who did that. He'd write a solution to the assignments he was giving us, and then spend another 5-8 hours on it trying to make it fit on a single overhead slide.
Early in my career, I took to heart such books and articles and often felt guilty and lessor-programmer when I cut corners. Here's my 2 cents now:
- Some of this is the coding equivalent of "6 rules for financial freedom" or "6 ways to find your dream soulmate". Generic advice that doesn't reflect highly nuanced reality.
- These rules are guidelines at best. There are justifiable reasons to break them; which I do often. Albeit this requires experience (and dare I say, wisdom). For example, refactoring code into a separate function levies a cost (of indirection) on the reader. Therefore copy-paste is sometimes fine.
- Clode "cleanliness" is a moving target. For a coder's mental health and value proposition for his project, he/she should know what code can afford to stay dirty.
Don't forget the most prominent part: 'your clean' and 'my clean' can differ greatly.
You can do your absolute worst and you will still find someone claiming there aren't enough comments, or the naming is bad, or the code is too dense, or the code isn't dense enough, or you should use typed objects instead of tuples and anonymous classes, or your code should be more functional, or your code should be more imperative, or it should be more event-driven, or it requires more logging, etc.
And it turns out, there is almost no research to tell you who is right and who is wrong. The only thing I can safely tell others, is all these discussions and additions will add 900% more work all things considered, and there's no guarantee it will be less bug free or more.
When he talks about his approach he typically mentions the importance placed on getting feedback from an actually working prototype as quickly as possible. Don't judge him by most of the ad-hoc code you've perhaps seen on twitch. But I've seen some really good-looking stuff there as well - straight to the point, no noise, not overabstracted. I would be interested what a finished project looks like.
I want code that is easy to read, write, update, and debug. It isn’t obvious that this means it should be ‘clean’, or indeed, what ‘clean’ is. Some other things described as clean code (eg uncle Bob) seem pretty bad to me. But then lots of people who complain about that also suggest things that seem bad. Perhaps lots of these things are too insignificant compared to other business or design decisions that one can’t really learn from experience or past successful or failed projects.
I enjoyed the article and agreed that working memory places a fundamental limit on the intelligibility of otherwise equivalent pieces of code. As a former psychologist with experience of memory research (though not quite this area), it might be useful to others if I add that:
- The size of the short-term store is normally said to be 7 plus or minus 2 (the 'magic' number 7)
- The Working Memory model has somewhat overtaken the 'short term' memory model, and it is unusual to see them being presented alongside each other like this (though 'short term memory' remains a useful, good-enough metaphor for explaining certain key aspects of memory)
- Chunking is typically viewed as a memory-supported division of stimuli (what you're reading, hearing etc.) into meaningful units based on LTM memory representations. A good example is a chess expert 'chunking' the layout of a chess board with many pieces in perhaps one or two units (e.g. 'It's the mid game configuration of [famous players] in [famous game], except the king's position is different'). We would expect more expert programmers to 'chunk' increasingly large units, I think (e.g. 'Oh, this is just the [famous sorting algorithm]').
- A single chunk is usually considered to take up a 'slot' in short term memory
If anyone wants papers/sources for the above, let me know.
Not high priority, but I'd love any references or names / key words I could look into it with.
I'm traditional wide comp sci by academic training, but spend my day job as a low-code enabler for non-programmers with varied backgrounds.
The working memory model explains and fits well with what I see them get and struggle with in day to day work, and I'd welcome references I could use to optimize my approach.
It's a bit tricky to tell what you're doing exactly - perhaps drop me an email if you have any queries (using this account; my original parent post was on a throwaway account because I had login problems).
I see what you mean - five will fit with more room left over. But much like a hand that has a capacity to hold 7 or so marbles, if you grasp only five at a time, your overall productivity will (almost certainly) slow.
Counterpoint: I'm sure most of us wrote far shorter sentences when we were learning our first (human) language, or perhaps even subsequent ones; yet now I suspect we can all read and write sentences with dozens of words.
Yet when it comes to programming languages, the majority of "advice" seems to be about absolute dumbing-down and propagating an attitude of "it's too hard, you can't possibly learn, just give up"?
I've always wondered about this dichotomy. Some languages like the APL family appear to have gone far into the "it's a language, to be learned like any other" territory, while more "mainstream" ones are drifting further in the opposite "don't even bother trying harder" direction.
(I looked at the one-line example in Python and, despite having very little experience in the language, it was actually faster to read and understand as a whole than the 3-line version.)
I know it was just used as an example, but in reality when I see code like this I think "I really don't want to reinvent URL parsing for this, I'll import the library for that instead," resulting in much cleaner code.
But I do agree on keeping individual lines, if not entire statements, small and simple. The ruby chainsaw is a great tool for that. I find a chain of simple statements arranged in a flow to be more readable than using lots of intermediate variables.
This seems a little light to me, doesn’t give me the feeling of being written by a veteran coder. Unless the idea is to dumb it down for a particular audience.
The real answer is write code like you write words: Rework it to make sense to the reader. How many newlines you need as a hint for your editor to
wrap and where you put them will fall out of that.
Perhaps a more helpful principle I heard a long time ago was that all methods are either a specific method doing a specific thing (like splitting up a string) or they call a series of methods of the first type. When we mix the two, it becomes harder to reason since type 1 is generally logically complex, so keeping these small makes them testable and readable, and the logic of the high level is more easily encapsulating as a series of DoThis(), DoThat(), ThenDoThat() calls.
If I've only helped one person today, it was worth it ;-)
There’s a balance to be struck if most of the Do methods need a common and/or interdependent set of parameters. Inlined code can be clearer because you can directly see how/why those parameters are used. You rarely have
DoThis();
DoThat();
DoTheOtherThing();
Instead you usually have something like:
x = DoThis(a, b, c);
y, z = DoThat(c, x, a);
w = DoTheOtherThing(a, z, x, y, b);
…and on top of that have to add error handling for those calls.
I have written a lot of Powershell in the last few years. I eschew the clever powershell ways of doing things if someone else may end up owning it (think: where-object, foreach-object) in favor of expressions that resemble other languages (foreach, for).
If I'm writing it for myself, and only ever myself, I'll use the more clever powershell ways of doing things. Expressions like:
1..10 | % {$_}
If you're coming from another language, you're going to have to run it to understand it, or look it up. That is time lost.
I primarily write in PowerShell for end-user shell tools and Go for network services.
Where-Object is going to let you cut down on the number of lines of code compared to foreach() and for(), and in my opinion will make the code more readable.
$vms | Where-Object -Property Name -match "sql"
vs
$vmOutput = @()
for($i = 0; $i -lt $vms.count; $i++) {
if($i.Name -match "sql"){
$vmOutput += $i
}
}
vs
$vmOutput = @()
foreach($vm in $vms){
if($vm.Name -match "sql"){
$vmOutput += $vm
}
}
For the Foreach-Object point, that cmdlet also give you the option to use begin{}, process{} and end{} blocks. So that you can with begin{} do something before any of your objects are processed, process your objects with process{}, and after all objects have been process do something with end{}. This logic with for and foreach would have to come before and after the for and foreach statements.
I don't see this as a "PowerShell being clever" but more as a PowerShell is a shell that uses pipelines like nix shells but it has everything as an object unlike nix shells. So you get to take advantage of that.
> PowerShell is a shell that uses pipelines like nix shells but it has everything as an object unlike nix shells. So you get to take advantage of that.
That was one of my favourite PWSH features when I was using it regularly. I’m a UNIX CLI-and-filter guy from way back and after using PWSH for a while I longed for the same power in bash (my shell for reasons of history, availability, and muscle memory, I’m unlikely to change).
use statically typed programming languages. Favor composition over inheritance . Develop bottom-up (reusable classes) instead of large-scale up-front design. SOLID principles (SRP being the most important). The Bottom-up approach also favors Unittesting. Code reviews, clear code formatting rules (simple editor plugins do the trick). Use static code analyzers. IMHO this kind of object-oriented programming leads to NEW code being written to implement features and NOT old code being tampered with. Ideally, the (tested) units of code (classes) have such clear responsibility that you do not have to touch them once they are implemented. If Super-classes begin to emerge, refactor.
First point is killer. There's little reason for dynamic languages nowadays outside of scientific computing (e.g. Jupyter notebooks). A decade ago static typing did incur a real overhead in verbosity, but with popular languages adopting type inference algorithms there's no excuse anymore IMHO. Static types have won.
I'm sure there are specific programs that would benefit from a treatment from these rules. However, there's one thing pretty fundamental to this article that I have a hard time agreeing with. And that is the notion that there are these three different memory types, two of which can only store "4 to 6" things.
I'm inclined to believe that there are probably many gradations of long vs. short term memory in the structure of the brain. In fact, I bet the gradations even vary by topic and of course depend on what sorts of tasks a person is accustomed to performing from day to day.
I imagine that the "4 to 6" figure fell out of a study that aggregated a large amount of data collected across subjects and that the figure itself can't capture much of the nuance or even the nuance of cohorts.
In other words, it may very well be that a large percentage of people who work professionally as software developers are capable of keeping more than 4 to 6 "facts" about code they're looking at in their head. But that they would also appear to have the same capacity as random people when it comes to arbitrary facts that one would be asked to memorize in a psychological study.
The starting assumption is highly dubious: "Short lines of code require less brainpower to read than long ones."
I'm not going to nitpick the incredibly bullshitty term "brainpower" and what is less and if that's actually advantageous, but if you write short lines of code, you're going to write more lines, which requires "more brainpower" to understand. You don't simply "chunk" lines in memory. If that were true, you could just as easily chunk function calls.
That memory plays a role is fairly certain. There is a pretty hard finding from psycholinguistics: it's hard to understand nested structures. The sentence "the rat the cat the cook hit chased escaped" is much harder to understand than it's right-branching equivalent "the cook hit the cat that chased the rat that escaped". However, reading code is not the same as reading natural language.
If you want to know if what you wrote is understandable, try reading your code without falling to back to remembering why you wrote it. Try to read what you wrote. Wait a few days if your recollections get in the way.
> (some more complex transformations, that only depends on mylist)
When you're reading the later stages of the code, you still have to maintain a memory of what "query_params" does, even though it's no longer relevant. That actually increases the burden on your working memory. The one-liner is more complex to understand initially, but it self-documents that the only info that is relevant to the downstream is the result of the map(...).
In general, the more variables that are declared in a code block, the more effort it is to understand, and the effect is probably superlinear with the number of variables. I'd say if you have to declare more than 5-6 variables, you should split into a separate function.
Give mysterious things room. In this case the most mysterious is [-3:]. That, together with the split, should have it's own line or maybe even multiple (function declaration, comment).
The mysterious part isn't what it does. The mysterious part is why. Why are we taking the last three url parameters? What are the meaning of those particular url parameters? Also url parameters are normally used as a unordered key=value dictionary, which makes it strange that we rely on a given order.
Functions come with abstraction overhead. You don't know who will consume them, so you may have to put up type checks, null checks other BS.
Also - functions split up the logic all over the place, it's confusing.
I think what we need are 'nested functions' which serve to kind of create a scope pushed to the stack - with an implicit 'return' - which we can then 'collapse' in the GUI etc..
I mean, it's purely cosmetic from a CS point of view, but it might help to organize things a bit better and hand off abstractions in long function implementations.
Huge projects with 1 or 2 line functions drive me crazy - you have to constantly jump around all over place to figure out what's going on. I actually believe it's a historic anti-pattern.
I make functions when we need 1) used in different places 2) meaningful abstraction.
Otherwise, well documented longer functions for me.
If there is some legitimate reason (say performance) to keep a tighter form (inline assembly, Python 1-liner, whatever), then making the unfurled equivalency as a comment nearby to allow the next developer to have a fighting chance would be really helpful. Also, error handling tends to be not included in the 1-liners.
The original code is perfectly readable until it does something completely unexpected, and the human parser has to start over to make sure they didn't miss anything. But unfortunately, the context for that "get the last 3 parts specifically" is never explained, so the entire line never makes sense. The human has to think a lot to come up with an (hopefully correct) explanation for the "why".
The solution isn't to extract every token from the expression to separate lines, but to document the "why" of the unexpected token. That can take many forms: a new variable with a meaningful name, a new function with a meaningful name, or a meaningful comment that warns the reader about the upcoming reason for getting just the last 3 parts.
The "bad" Python code in that example is perfectly fine. I'm not a Python programmer but I can read Python a little bit, and the example uses basic programing concepts like string splitting and array ranges.
If you don't understand that, multiple smaller lines won't help you, because you just don't know what you are doing.
In addition, that code example is easily testable. Testability is more important than readability in modern programs that follow modern CI/CD principles -- and the readability is not really that bad either. Also, modern debuggers don't have issues with nested/lambda statements like these.
If the article's author had a legitimate bone to pick, they would have better examples.
The question isn't if you can figure it out, but how long does it take you? The simple version might take me 2 seconds to read. The original version might take me 10 to 15 seconds. Multiply that out over a day and you're hurting quite a bit.
This is the main reason I don’t like arrow functions in JavaScript. People overuse them to create “clever” code - lots of things going on in a single line. Then they try to claim that by having everything on a single line the code is easier to read and understand.
If I use elaborate camel-case variable names, it seems to reduce the load on my short-term memory because I don't have to remember what a variable name represents. It's meaning is there when I need it and can be forgotten otherwise.
Then you have to name things. And naming things sucks, especially because not every intermediate has an obvious name for it, distinct enough to distinguish it from the next intermediate chunk.
Ok, but if you're reading code for the first time, you're going to have to store the intermediate parts in your working memory somewhere. And if you don't have a mnemonic, like, a name, then "the return value of the lambda after a split" is a lot harder to remember.
At least for the contrived example from the article, the solution isn't to break up the code, but to use denser code. Use a regex.
Does anybody really think that e.g. sregex[1] is better than just learning and using the regex language directly? Because that's where this kind of thinking leads.
I know it's not the point the author is trying to make, but I couldn't help get the feeling this example isn't good enough to carry the point.
from stdlib:
from urllib.parse import urlparse, parse_qsl
url = 'https://www.example.com/some_pathsome_key=some_value&foo=bar'
parsed_url = urlparse(url)
values = [v for _, v in parse_qsl(parsed_url.query)]
print(values)
In this case `query_params` works well, but it's sometimes hard to find descriptive and reasonably concise names for the intermediate value. In those cases, the ideal would be using only postfix chaining, so that you can read it by only keeping the intermediate value and the next operation in mind:
i don't really see why or how a generator expression is any easier to read than a chained call like the OP's example.
In fact, for people unfamiliar with python, this expression is even more strange - you read expression starting from the middle (the _in_ ... part), and then return to the beginning. It makes your eye dart forward and backwards on the text.
When I clicked on “More settings” in the cookie dialog, it displayed a loading animation (ignoring my prefers-reduced-motion setting) and got stuck. Just straight-up user-hostile design.
Gah. I've seen the other side of this, a few people far too trigger happy to make FivePlusVeryLongNounVO/DTO for every little thing, and it gave me some new appreciation towards tuples and primitives. Sometimes you really don't want to go into another new file for an object type which is used in only one specific place. Especially with
>Don’t Abbreviate
Meaning the variable name will end up long anyway. With tuples, you get deconstruction without the hassle, too.
> Gah. I've seen the other side of this, a few people far too trigger happy to make FivePlusVeryLongNounVO/DTO for every little thing, and it gave me some new appreciation towards tuples and primitives. Sometimes you really don't want to go into another new file for an object type which is used in only one specific place.
The rules are an exercise for a toy project. Like all similar 'rules' they are just hints to make you think. When done with the exercise, and you see a string with a social security number in a production code, you may consider creating a dedicated SocialSecurityNumber class. The class will guarantee a well formed social security number according to official rules. The class may even offer Area, Group and Serial parts of the social as separate fields. The class may decide to use a string or integers internally, but that would never be exposed to the class consumers. All the code that uses SocialSecurityNumber will not have to guess whether string is valid, if it has dashes etc. The same reason you use built-in types like an Integer (as oppose to a tuple of 4 bytes or 32 bits).
Your example breaks down the moment you put it back in context. SocialSecurityNumber works because it's going to be used across multiple subcontexts. The same reason Vector2 works over using (x, y) tuples everywhere.
I'm specifically mentioning one specific place. This now requires you to go to a different file to see what's up, for every time you need HighlySpecificModelWithOnlyStringAndInt, and most naming doesn't help provide context. All that's happening here is moving vertical navigation to navigating different files. It's a remnant of a time people wanted more specification than object[], but valuetuples didn't exist yet (C#).
And even then, SSN is a pretty well known abbreviation. So your example is also far too favorable compared to most extreme yet real examples.
You seem to be fixated at knowing what objects consist of and you seem to stop at a fairly arbitrary level - primitives provided by the language (int, string etc). Why not go all the way down to bits? I find that a well designed Value object [0] makes me more productive because I specifically don't need to know how it is implemented internally, only the exposed interface. Examples:
ZonedDateTime
PhoneNumber
SocialSecurity
Guid
EmailAddress
Point
Url
ExpiredCoupon
Regarding one specific place, if I only need to know date time and its zone in one specific place, would you recommend Tuple<int, int, int, int, int, int, int, string> instead of ZonedDateTime?
One level of indentation just leads to an explosion of tiny one-use methods with weird names, and now you can't read the code linearly. You will almost certainly never reuse these tiny methods, especially since you're likely consigning them to an instance of a class instead of a free function, so all you've done is forced people to jump around a lot.
> 2. Don’t Use The ELSE Keyword
Not using the else statement just obscures the fact that there's a branch in the code. Obscuring something important seems to be the opposite of what you should do.
> 3. Wrap All Primitives And Strings
Ugh, that seems verbose and clunky, especially in a language like Java without operator overloading. I'm all for type aliases or typedef's, or, creating a class if the builtin primitives don't work (I think a Money class makes sense because you don't exactly want to use a float, for instance). But just putting wrappers all over the place sounds grotesque.
> 4. First Class Collections: Any class that contains a collection should contain no other member variables
Why even have a class then? Why not just have functions that operate on a collection? It's much more generic that way, since if you're using iterators or an abstract collection interface, you can potentially allow the user to choose the exact data structure, and you avoid the ceremony of creating a new type that's again just a wrapper.
> 5. One Dot Per Line... Basically, the rule says that you should not chain method calls.
This is the first one I roughly agree with, but I wouldn't consider it a hard rule. Chaining .map and .filter together for instance is a very common pattern.
> 6. Don’t Abbreviate
min/max is just as clear as minimum and maximum. I'm not using "index" in my for loop when "i" will do. "n" is perfectly well understood as a count of things. Abbreviations when used properly make code easier to read, not harder.
> 7. Keep All Entities Small... No class over 50 lines and no package over 10 files
Ok, assuming the problem can't be simplified, all you've done is now fractured all that functionality into tens/hundreds of files. How is that easier to follow? Sure, there's balance in all things, but I'd probably rather read a 1000 line class than 20 small files split over 2 packages.
> 8. No Classes With More Than Two Instance Variables... I thought people would yell at me while introducing this rule, but it didn’t happen
They were being polite. I'll do it for them. What the fuck?
The example he gives is also awful, where instead of using a string for name, he makes Name a type (ugh) with FirstName and LastName. Not only is that overly ceremonial, but it's wrong, there are plenty of names from various cultures that do not fit cleanly into FirstName and LastName. Also, what happens if he wants to store a MiddleName? That's three instance variables! Ohno! OR what if the person has like 10 middle names (this shit happens). Are we going to have 5 nested data types for that?
> 9. No Getters/Setters/Properties ... My favorite rule. It could be rephrased as Tell, don’t ask.
My brain feels like it's going to explode.
> It is okay to use accessors to get the state of an object, as long as you don’t use the result to make decisions outside the object.
Why else would you want to get the state of an object?
> Any decisions based entirely upon the state of one object should be made inside the object itself.
If your classes are 50 lines long, I guarantee you that other classes will be making decisions on other objects behalf.
> Then again, they violate the Open/Closed Principle.
I think the industry is largely realizing that this is a bad principle, as it implies inheritance. I think most people outside the enterprise java world now realize that using interfaces or free functions is largely better.
Please keep in mind that these were rules that you apply to a _toy_ project. You apply the rules once, blindly, even if they don't make sense to you at the moment. Then, when you work on a real thing, you may remember, for example, to create a dedicated PhoneNumber class with strict rules (e.g. E164) instead of a string that gets shuffled around with no one really knowing whats inside. Or you just forget the rules as a nonsense and move on.
You seem to be criticizing the 'rules' as if they are suggested for production code. You couldn't be seriously thinking someone suggest maximum-of-2-fields as some sort guideline for the real world.
The article starts with some reasonable premises, but the conclusion does not follow.
I think most APL programmers would disagree with this take. Dense code has real advantages, and naming everything has real costs that are hard to see. There's nothing magic about a "line" that suddenly allows for chunking. You have to build a parse tree in your head in any case.
I'm reminded of Doug McIlroy's challenge to Knuth.[1] It's worth a read.
Would you rather have 6 lines of dense shell, or 10 pages of Fabergé egg? I'll take the shell, thanks.
Look at the source code for J (an APL derivative)[2]. It's written in C, but that C was written in APL style by APL programmers. Lines leverage macros and 1–2 character names, making them extremely dense. Some files have a comment on nearly every line. For an average C programmer, this code looks absolutely insane. But it's not. The J devs find this perfectly readable and maintainable. It's clean code! If written with the typical C idioms, it could easily be 10x as long, and therefore harder to maintain. Your first impression is a snap judgement due to a difference of culture. You can learn to read this style with practice. Whatever your current style, that took practice too.
I think the comparative rarity of APL compared to every other programming language in existence says a lot. Even if I were an expert in APL, I can't think of a single place where I could get a job writing it.
I think language popularity in industry mostly comes down to path dependence[1]. It doesn't say as much as you seem to think.
A few approaches got lucky in the rapid inflationary period of the personal computer revolution (C), and the advent of the Web (Javascript), and became deeply entrenched in industry, while superior alternatives that had been known for decades missed the boat. Industry languages still haven't caught up to where Lisp, Prolog, Smalltalk, and APL were in the 1970's, but they are clearly (if slowly) trending in that direction.
APL and derivatives are still used extensively in finance, a highly competitive field, to say the least. That's where you find the jobs.
I'm not saying popularity = good, what I'm saying is that most people aren't smart or patient enough to use something like APL. Most people have a limit to the amount of density they're willing to deal with.
Well, I can only hypothesize, but APL is a lot like math, visually anyway. Most people are very intimidated by math.
Math tends to be written for subject matter experts. If I'm reading a paper about graphics, I know that θ is "theta" and likely means "angle in radians". If I'm not a subject matter expert, it means nothing. Of course they can write at the top "let θ = light incident angle in radians", but I still need to hold that definition in my head while I'm trying to understand the rest of it. If I simultaneously need to remember ⍵ and ε and Δ, well, I can do that if I've seen those symbols used in similar ways previously, but it's certainly going to increase the amount of things I need to hold in my head if it's something novel. And now I have to memorize what those things mean, and I suck at memorizing personally.
In contrast, most mainstream programming languages you'd just write "angle_rads" or some variant. It's a little longer, but I don't need to remember nearly as much because it's just plain english formatted in a way that at least resembles sentences I might read. I just think APL is too unfamiliar for it to be easy to learn, and so the only people that are going to learn it are those that are very intellectually curious. That's a good thing, but if you're talking popularity? Well, I think we've seen it, and it's not just path dependence.
All comes down to good naming in the end. The craft is finding both compact and specific names.
I think the mantra for all names to be short can be counterproductive here. If the code span of a variable is short, a long name can be fine (and very clarifying, perhaps even resulting in a comment not being needed).
Shorter names for longer spans are much better. But you’d hope they’re the very obvious subject of that span.
Although I agree the original line is a bit long, and the first refactoring is a clearly more readable, but after that it starts to feel like bike-shedding. FWIW I don't believe in refactoring things into tiny methods that are just used once—it's a lot of boilerplate which makes zero sense if you are not going to reuse it, but it's not the hill I'm going to die on.
Overall a lot of this boils down to minor style issues. I care very little if you give me 5 short lines with named intermediate steps versus a dense one-liner, however I do care very much if your code leverages pure functions, minimizes cyclomatic complexity, encapsulates messy bits, and has some form of test coverage. The former might take me a minute or two longer to grok (depending on my personal context), but the latter compounded over a wide surface area can lead to a completely unmaintainable system and a pathological fear of touching anything.
I think this article is missing the forest for the trees.
I've found that dividing software into layers, and making sure that each file relies on the same set of invariants from its dependencies, and also maintains a (different) consistent set of invariants for its callers works much better.
For instance, I'd prefer a function that takes a string and confirms it is a valid URL.
That would delegate to URL character esacaping logic and DNS validation. (Are & or ? valid DNS name characters? Will they be in the future? I neither know nor care.)
On top of that, there would be a parser for key=value config file lines.
Then, the example in the article becomes something like:
keyvalue = parseConfLine(input)
URL(keyvalue.value).params[-3]
Plus a few more lines to confirm key is as expected and that value has enough query parameters.
Alternatively, I'd use a perl oneliner with a regexp. I see no purpose for code that lands in the middle ground between these extremes.
A reviewer can usually tell which code is easier to understand side by side, even when it's yourself as the reviewer.
Applying rules like these to your code may or may not result to cleaner code, but that's a testable hypothesis.
I've seen all too often some clean code recommendation or other applied to code and it gets harder to understand. And the person doing the refactoring (often myself) gets caught in sunk cost.
Now my recommendation is always:
1. Use your intuition to predict if a change makes code cleaner.
2. Try to make that change, and be open to doing things a little differently that you first imagined.
3. Test your hypothesis to see what others think. Decide what to do, but be mentally willing to throw it away.
4. Repeat
Articles like this are good resources to help train your intuition, but there is no substitute to developing your personal and team "flavor profile" for what styles suit your way of thinking.
Writing "clean" code is more of an art form, you can't really have easy rules.
I think the general idea is that clean code is short code, that's the base guideline. Generally shorter code does less things, reducing cognitive load. It may also have performance benefits. It also takes less space on-screen, which is also a good thing: less scrolling, ability to use bigger, more readable fonts, etc... And as explained, short-term memory is limited. Short code also tends not to repeat itself, another common advise.
But that's the baseline, all the art is in appropriate breaking of that guideline, to have short code that doesn't look like it came out of a minifier.
Splitting lines makes longer code, bad, but sometimes it is justified. So what is your justification? The article focuses on "one liners" being hard to understand, but really, it depends on many things. For example you may use a longer form if you think that it is an essential part of your code and it is critical that you should pay attention to it. On the other hand, you can use a shorter form if it is a common pattern, what is "common" depends on who is going to read your code, or the project you are working on. For example, bit manipulation can make a good part of your code base, or be a one-off thing and it will have an influence on how you write that code.
Moving code into functions is generally a good thing if that function is used often (shorter code). I think it is the origin for the term "refactoring": factoring ax+bx+cx+dx becomes x(a+b+c+d), only a single "x" remains and it is shorter. But if that function is only called once, of if the operation is hard to extract from its context, it can lead to longer, harder to understand code, and again you have to exercise judgment. For example you may want to write a specific function because it is a tricky, specific part that you want to separate from the boilerplate. There are interesting considerations to using functions, because it actually reorders code, for example "a(){do_x}; do_y; a(); do_z" is written as x,y,z and does y,x,z, which is often, but not always unintuitive.
The examples and improvements in the article feel obvious like “common sense” — which is a good thing. I’m not 100% sold on the reasoning though. It kind of feels like a just-so explanation without much justification
> STM and WM are small. Both can only store about 4 to 6 things at a time!
I'm sorry but this is such an asinine statement. Your brain doesn't store "things" and the number of working items depends on so many factors the complexity of the information, the level of association between items, the attention span of the individual, which can be trained, and a multitude of other things.
Neuroscience is a useful tool for self-programming but you must be careful peddling absolutist statements like this which can do more harm than good.
I'm really tired of hearing the 4 items +/- 2 being parroted around in cases like these. The studies that come to that number are basically "Remember these completely arbitrary things such as numbers or words in order". That's nothing like reading lines of codes where you have variable names, and you're able to construct meaning and relationship between the things in your mind.
Sure, it might be relevant if all variables were named "x", "xx", "xx", - but they're not.
A map lambda example is what I had in mind when reading the article. I'm not a big fan of the temporary variables, though.
Admittedly the example below is not a perfect solution, but that's where I thought the article was heading when splitting that code over multiple lines for readability.
This seems like an interesting heuristic for anything that could automatically either generate or "format" code - a little more semantic than just relying on a text parser
As other people have noted, this seems like a criticism of expression-heavy languages. I'm not sure the working memory idea really is a good argument for short lines. Assembly language is entirely short statements, and has only a few operands per line, but is so tedious to read because of how much state/working memory is occupied. Complex expressions can actually reduce the mental overhead by reducing the number of used variables to a minimum.
For the example in the text I'd typically just include a one line comment above to show what an example string would look like and leave the code as is
Ok, quick rules that focus on single lines. That's neat, but from experience most of the complexity comes from the structure more than just how the code is written, conventions about how to write a line of code won't fix corrupt indirections, misplaced coupling, lack of cohesion, undue repetitions, missing tests, etc.
Clean code is not just a few rules about how to write a line. You can write nice lines that still don't make sense and amount to shit code
Ah yes. I remember when I read the Clean Code about 10 years ago and produced the "cleanest" code I had ever seen – only to have it destroyed by a senior dev during a code review because the task was relatively complex and my overall structure was garbage. One of the saddest days of my career. Probably the most helpful day of my career too.
The calculation of query_params, having no dependency on the lambda parameters or anything being mutated, has been lifted out of the lambda, and thus spared from repeated execution by map. The compiler for that language won't do this automatically.
What? No it isn't! You didn't parse that correctly.
The query params were never in the lambda to begin with. Python function calls have strict (not lazy) semantics, i.e. "applicative order", i.e. both expressions passed as arguments to map() are evaluated before the map body gets them as parameters, thus the query params would only be evaluated once, even when inlined as they were originally.
Same with the lambda definition: it's evaluated only once. It's just the lambda body that gets reevaluated each loop, and only evaluated for the first time on the first loop.
Sorry; it looked to me like x.split('=')[1], query_params is a tuple being returned by the lambda. But of course that leaves map without the needed argument.
The issue is that short code lines increases the length of code aka wastes vertical screen reasl estate aka visible code, so you're overburdened short term memory has to context switch to scroll.
"Simple, put code in small methods"
Oh great, now I do a nav jump or a string search as a context switch rather than scroll.
Coming from a background in lower level languages where you can only express one simple thing per line my tendency has always been to be more verbose than my colleagues. The worst time I ever had was when I had to work on someone's Perl code that one time. So I really like this heuristic to make code more readable.
I always liked the quote "you need to be twice as smart to debug a code. If you write smart code, you, by definition, cannot debug it" (sorry I have no idea who said this). This is why I still code in C. No smartass bullshit, just plain old undefined behaviour and out of bounds access. Lovin it.
This is from Brian Kernighan (the 'K' in the "K&R C Book" and AWK), known as Kernighan's law:
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it."
The main reason I stopped using the old school for loop in JavaScript is that it's doing too much in a single line. If I can't do for-of, I much prefer a while loop because it does effectively the same job as for-in but each step gets its own line. I find it easier to follow at a glance.
I think a contrived example. In truth, there is much more likely to be a way of tidying this up with a nice reuseable function like "get_querystring_params" which returns an array and then take the first 3 with a comment like "only the first 3 parameters are used for the search".
Taking a subet of query params smells in its own right so, again, might be a bad example.
The suggestions here are so not 1337. The whole point of writing code is to show off how much smarter you are. During code reviews, you can teach everyone else a lesson; you’re basically doing them a favor by making them read your 1337 code. If they can’t read your code they aren’t your equal.
This is so subjective. Some people do want to write such code as that is “cleaner” because it’s compact. Some wants to explain every single step because that’s “cleaner”. Some tries to do something in between and it’s somehow “cleaner”. But in the end, it’s mostly subjective.
Can you objectively say that familiarity isn't the primary reason APL is less understandable?
Put another way, if you had 2 years of APL experience and 2 years of Python experience, would you maintain that the APL code is objectively less clear?
Is it really fair to judge languages we aren't familiar with to the extent of the language we are comparing to?
Well, it'd be fairly easy to test: teach a first year coding course, give one group python and the other group APL, and monitor the grades and the drop rate.
I can't prove it, but I'm highly confident that python would win. Because it has a lot of analogies to things people already know:
- In simple cases, you can read it as a set of commands, like a recipe. People are familiar with recipes
- Syntax mostly uses well know words (or abbreviations of well known words), or very simple symbols that you'd pick up in basic math (+,-, *, comma, etc.)
- Variable names make clear what the result of a particular expression maps back to in the real world (ideally)
- The whitespace indented structure of the code is fairly obvious if you've ever written an outline
APL has...
- Unfamiliar symbols
- Terse, confusing names
I mean, look at this example for getting the average from a list of numbers:
{(+⌿⍵)÷≢⍵}
Since I don't know greek or math particularly well, in my head I read that as "plus minus sign with a slash through it and curly w divided by triple equals curly w with a slash through it". I'm nowhere.
I couldn't write that if I didn't know python, but if I had never written a line of code in my life I could give you a decent guess as to what it does. Is it verbose? I guess in character count, but not in any meaningful sense.
You're right, familiarity IS why python is easier to understand. But it's not programming familiarity, it's that almost every aspect of it is more familiar. The only people that (might) find APL more familiar are mathematicians in my opinion, and they could easily understand python.
I know a few people who've taught APL to kids or other non-programmers and the most common remark is how much quicker they get it than students who've worked with other languages. I don't know what the result of the experiment you describe would be but you're definitely biased by your experience. And as a counterpoint to your example, here's the code to multiply several numbers by two in both Python in APL:
x = [1,2,3]
[n*2 for n in x]
2 × 1 2 3
We have forums: APL Farm is the most consistently active. If you'd like to talk to real live APLers just hop in.
Well, to you it is, for someone else it’s “clean”. APL is a fun language and kind of my point here. Someone thought that other languages were too verbose and tried to write a language that was more expressive which a “normal” language never can achieve.
While I'm not going to go collect a bunch of APL programmers to confirm this (where would one even find them?), I highly doubt that claim. Knowing a language doesn't mean dense code is suddenly obvious.
This is a silly example, but years ago I wanted to prove that you could write a non-trivial program in python using a single expression (because python's lambda only allows you to use expressions, not statements). And I managed to do that. And it's hideous. Any python programmer could theoretically understand what this is doing, but I doubt they would. Bonus points if you can guess what it does without running it!
(lambda:
not globals().__setitem__('sys', __import__('sys'))
and not globals().__setitem__('this', sys.modules[globals()['__name__']])
and not globals().__setitem__('time', __import__('time'))
and
#program
[setattr(this, k, v) for k,v in {
'set_color': (lambda c: w(['*', ' '][c])),
'abs': (lambda t: (int(t) + (int(t) >> 31)) ^ (int(t) >> 31)),
'w': lambda x: sys.stdout.write(x) == 0,
'smash': (lambda t: -((t * -1) >> 31)),
'color': (lambda n,k: set_color(smash (k & (n - k)))),
'col': (lambda n, k: k <= n and not color(n,k) and col(n,k + 1)),
'row': (lambda n: (
not w(' ' * (40-abs(int(n/2))))
and (col(abs(n), 0) or True)
and not w("\n")
and (abs(n) < 63 or n < 0)
and not time.sleep(0.05)
and row(n+1))),
'triangle': lambda: row(-60)
}.items() ] and triangle() )()
You don't need a bunch - you need 1. I am an APL programmer, and I can tell you 100% for certain that anyone who knows any APL would find that easy to read. (Want to find more though? Go to one of these: https://aplwiki.com/wiki/Chat_rooms_and_forums)
Knowing a language absolutely does mean 'dense' code is obvious (fwiw that APL code is not at all dense, you can do go a lot shorter). It's comparable to chinese characters or something like that. It only looks dense/unreadable to someone who doesn't know it.
Well, of course that code was meant as a joke, and I did some things intentionally weird. I mention it just to point out that a metric like "it's only a single expression!" doesn't mean that something is actually simple.
Engineers would benefit from talking to designers more often. The rule of 5 +/- 2 has been around in UX design forever. When you write code for others you're designing a human interface for solving a problem.
«Every line does only one thing» - that’s not related to real clean code. And there are bunch of languages that’s OK to have few things on the same line - perl, ruby, groovy, scala and even php.
My, the very dark pattern in the cookie dialog... closed the page when manage cookies didn't load in 20 seconds. I bet there's an explicit delay in there.
The fact that the site uses a consent solution that fakes a loading screen when trying to configure (read disable) tracking/advertising is an instant bounce for me.
in the example given, I started with 10 things to keep in working memory. now that we've added a named function or a named variable, we have 11. I suggest it is at least as important to name things well (or add comments) as it is to break lines up.
A nice feature of working memory is that although it's limited to around 6† "things", each thing can be any size, if your brain considers it a single unit. This is called "chunking".
So, it's easier to remember the three numbers 34, 765, 812 than the eight numbers 3, 4, 7, 6, 5, 8, 1, 2.
Refactoring code into separate functions with descriptive titles is probably a lot like combining numbers.
---
† Well, the article says 4–6; I'd always heard the average was around 7.
I'm working on a project that is following Uncle Bob's Clean Code guidelines of striving to having functions be ideally 3 lines or less, and nor more than say 7. I have mixed feelings about it.
My initial prejudices have largely held. I do find the code harder to read and follow. Having to jump around, follow variables that change name as they are passed through functions, keeping track of state that was moved to a class member rather than in a function body (because breaking into pure functions resulted in too many function parameters). I can't fit as much code on screen because of all the additional function definitions.
Lastly, the success of the method relies heavily on how well you name your functions, which is often considered one of the hardest parts of programming. A name that makes perfect sense to me may not be as clear to others, or even to myself in two months. And the devil is in the details - there are so many implied semantic preconditions and postconditions with every function you write, there is no way to fit that into a function signature no matter how well chosen, and if you tried to document them them all your comments would be larger than code itself at this level of granularity. So you still end up having to read all the called code to understand the details of what is happening anyway, which is easier to do with more flat code.
On the other hand, I've found the process of "extract till you drop" to be very helpful in forcing me to find ways to clean up my code. It naturally tends towards maintaining separation of concerns, finding ways to DRY when the initial structure wasn't conducive to it, and generally disentangling things even more so than when I try to refactor to meet these goals directly. If I had all the time in the world on other projects, I think I would apply "extract till you drop" on my code, then after it is disentangled, recombine it back into reasonable size chunks.
I'm not going to disagree completely with @pavon. He's stating his lived experience and I respect that. Maybe people are different in this regard. I'm more in line w/ @hardware2win. More smaller functions just seem to add to the cognitive load. But I am a fan of cascading a bunch of message sends / method invocations. I've never had a problem with that.
People seem to be passionate about their answers, now I want to get the programmer psychology book and see if there's data about people being more effective with or without a lot of small functions.
Those guidelines should (my personal position) be taken as advisory, and never as hard rules. Functions should be “small enough, that a less than gifted can understand it”. Is difficult to measure in lines. Best example is a big switch with 10 cases. Artificially breaking that in smaller pieces is not helpful.
I have a soft rule of 3 to 7 different control structures (if, for, case, etc) in total, and 2 or 3 nested.
You forgot a KEY property of a function: this is an abstraction. You don't (and in most cases shouldn't) care how this "black box" does what it does, you identify it by the name and move on. So a function collapses N things to understand to 1, not how you have described it.
Exactly. At each level of abstraction, you don’t care what the functions look inside. The name must be enough to understand what it does.
You can of course descend one level, but at that point the layer above is no important. Just that the function fills the contract, possibly calling. Still more functions, which at that level are black boxes…
No, it was rhetorical, because it's obviously (to an APL-family programmer), not bad!
Your cultural prejudice is showing. There are good reasons APL is written the way it is, and this example is simply bringing those benefits to C by writing it in the dense APL style. There are other APL derivatives, like J[1] that are written in C the same way. These projects are well-maintained. They aren't collapsing under a load of technical debt. The style works. To them, it's clean code.
My cultural prejudice for readable code? Yes I guess it is.
As far as I can tell the main reasons for APL being written like it is are 1. it's ancient, from the world of teletype where character count was way more critical, and 2. some programmers love code-golf write-only syntax.
It reminds me a lot of regex. You could say "there are good reasons regex is so terse" and "people successfully use regex all the time" but that doesn't change the fact that it is a very write-only syntax and would be much better if it was more verbose. There are actually a lot of recent efforts to do that.
The jsource repo you linked seems to have had only 4 contributors ever, which suggests to me that it is not a popular style and not easy to read.
As far as I can tell APL had some interesting ideas in terms of data manipulation, but there's no reason those ideas have to be expressed all on one line with no comments or spaces.
I’m not sure what you’re trying to say, but that’s not true at all. There are other metrics for code complexity, including fairly simple but useful ones like number of logical branches.
I skimmed the article and missed the definition as well. Now that's certainly my fault, and I knew that if my curiosity were peaked I'd go back and read the article more carefully.
But I'd also like to say that, IMHO, the fonts, spacing, various headings, images and codeblocks with dark background are all mixed up on that page, and the *bold* text does not stand out at all. Every other sentence or piece of information on that page is highlighted in its own way.
In this case there's, I think, a better alternative; the equivalent-ish code in Ruby for the example code here would be something like this:
You can write these nice functional pipelines where you just read the code top-to-bottom and see step-by-step what is being done to the data on each line. You don't have to jump up-and-down around the code when reading it, and you don't have to keep too much context in your head when reading it.This is one of the reasons why I vastly prefer Ruby over Python for most data processing tasks. I wish more languages would support this style of programming.