351 comments

[ 2.1 ms ] story [ 310 ms ] thread
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.

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.

>guidelines of striving to having functions be ideally 3 lines or less, and nor more than say 7. I have mixed feelings about it.

Dont feel bad about it

Those small functions with hard limits are just terrible advice

When you gotta know functions impl., which for me is very often

Then this approach just increases cognitive load

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.
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.

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…
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
That is a cool idea. Show the code the way you understand it best without even having to change the underlying text.
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
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.
Code has a typo ‘slit’ instead of split after bringing up the concept of moving something into a function.
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 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.
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.

Skimmed the article and the comments and got no answer - can someone tell me what the rule of six is?
The rule is literally described, in bold text, within the article.

Try actually reading instead of skimming next time. Or at least skim more slowly.

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.

>That gives us a rule for deciding if a line of code is too complex:

>A line of code containing 6+ pieces of information should be simplified.

he put it in bold.

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.
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.
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 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.
Or go spelunking through modern Go code.

Variable names that are so terse it hinders reading and comprehension.

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.

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.

It would presumably be evident from the context in which the `map` call appears what its purpose is.
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.
That's precisely what we should expect from the context of the map call, but that context is outside the scope of the example.
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!
> For some reason it's just the the final 3 values they want (both weird requirements). Make that clear from the code!

Yeh, I mean the name should really tell you what those expected to be.

`hopefully_auth_param_values` is almost a better name

Just be consistent, whatever it is.
short names

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.

Short names are easier to read, because they fit on fewer lines. Doubly so if the statement fits on one line.
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.

I tend to think length should be proportionate to broadness of usage within the file/project

If it's used in lots of places, go for MAXIMUM_MONSTER_DENSITY_SCORE_MULTIPLIER

If it's used in a single file/function, then maxDensityMultiplier might be okay. (You can infer the rest from context).

If it's used in a very confined section, then multiplier.

If it's a loop variable then, sure use m.

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.
If they all start with 'factory', sure.

I would be a long name that doesn't add information that helps differentiate between different variables.

(comment deleted)
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.
Crisis == opportunity ツ

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.
the only known metric for code complexity is as number of lines grows complexity grows
And that's... bad? Culture shock, sure, but this looks like fairly clean APL-style C to me. I would have wrapped some of those lines though.
Of course it's bad. Is that a serious question?
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.

[1]: https://github.com/jsoftware/jsource

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.

If the thesis is that fewer lines = less complex, APL-style C is the blackest swan I ever saw flying past me.

Is it bad? Apparently it's possible to write software in this style, so it can't be that bad, but it sure takes some getting used to.

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 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.
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?

Or, in example with hilariously apropos incomplete post-hoc comments, 0x5F3759DF https://en.m.wikipedia.org/wiki/Fast_inverse_square_root#Ove...

Often, breaking out a well named variable or three is the better way to make code readable.
> There's a real danger that comments aren't updated when code is, particularly if 3rd parties make those changes

That problem exists because no one denies pull requests or rejects code that has outdated comments.

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.

He was a much beloved professor.

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.

So you can call those: “50 cent expressions”

They help no one but their ego…

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.
At least in Haskell, I feel like this doesn't hold true.

Typically more concise code takes advantage of core language abstractions.

It is actually simpler unless you are unfamilar with core language abstractions, but I'd argue that's a you problem.

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.

It's a brilliant quote, though.

(comment deleted)
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 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?

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.

As an example, given:

  const sales = [ {month: "Jan", day: 1, total: 120 }, ... ]
You could determine, say, the highest sales day of a given month as follows:

  const highestSalesDayByMonth = _.chain(sales)
    .groupBy("month")
    .mapValues((salesForMonth) => _.maxBy(salesForMonth, "total"))
    .mapValues("total")
    .value()

  // highestSalesDayByMonth = { Jan: 140, Feb: 90, ... }

Naturally, minimizing the complexity of the iteratee functions and carefully naming of their arguments is very important to ease debuggability.
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.

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.
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 :(

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."

Never cleaned up the most obtuse part of that code snippet - why are we only keeping the last three parameters?
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.

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.

Some things are short and self explanatory though. `1..10` is just syntax sugar for a stream or list from 1-10 right?
1..10 is powershell range operator, and % is an alias for foreach-object.
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).

I can't access the linked page without accepting cookies.
Can't you just use an incognito tab? It'll delete all the cookies when you're done.
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).
Right?! That should instead be -len("foo") or -NUM_PREFIX_PARAMS
That doesn't improve anything in terms of knowing why the number is there.
len("foo") certainly does, and the global constant is a great place to put a comment, if the name isn't descriptive enough.
> [-3:].

That kind of index notation really isn't mysterious if you write a lot of python in my experience.

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.
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

The first quote is an over-simplification that often does not hold in practice: one usually needs both. The exceptions are mostly trivial programs.
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.
It's the reason Excel is easier to learn than Java.
> 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.

Well, dealing with graph algorithms, none of them are obvious from the representation. Which is usually a matrix.

In fact, Gauss Jordan is also not obvious, just seeing a matrix.

What is easier to read:

a) 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1

b) 20

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:

https://norvig.com/spell-correct.html

What the f* is wrong with you?
(comment deleted)
If your comment were code, I think it's an example of a)
Fair enough, but a strong point had to be made.
Your example is artificially designed to make your point, but imagine a case like this:

  # Add the left & right margins
  width = calculatedWidth + 1 + 1
The "+1+1" might indeed be more readable than "+2".
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.

You miss the point. The problem are not arithmetic operations.
Sorry if I've misunderstood your post.

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".

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.

(comment deleted)
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.
(comment deleted)
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.

Looks similar to Streams in Java. We try to use that style where appropriate as it is much more readable and compact than imperative style imo.
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.
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?
I like this style too, though it can make debugging trickier.
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.
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

    .map { | *String* key_value | key_value.partition... }
where String shows up in light grey text indicating that IJ knows `key_value` is a String
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.

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!.

> editor support should not become a prerequisite for existing code to be comprehensible.

I don't know... i'm frequently torn deciding between whether i think this is true or not.

On another note, readability of code is less about coding style and more about familiarity.

> 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.

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.

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.
Emphasis on well-named. Naming things is hard.

Maybe not relevant in simple toy examples, but you don't have to look far until to find a function that isn't so easy to name.

its also a level of indirection... It's sad but functions often slow performance down. but this depends on the language.
Can't be many languages where that matters in 2022. Auto-inlining of short functions was a thing in the 1990s.
This may be true for compiled langs. In interpreted langs this is not the case. Function calls in Python for example are notoriously expensive.
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.

[0]: https://github.com/Nomarian/crapbenchmarks/tree/main/call

  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'
If you're hand-wringing about the performance impact of function calls, Python is probably the wrong choice anyway.
(comment deleted)
Emphasis also on "bunch of lines".

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.

One-line functions are also nice when a comment would've been warranted otherwise. But yes, easy to go overboard with it.
> Naming things is hard

Naming is hard unless you have a sensible concept hierarchy, and then naming is easy.

Concept hierarchies are hard, unless you have a sensible system model, and then concept hierarchies are easy.

System models are hard, unless you have domain expertise, and then system models are easy.

Domain expertise is hard.
Not everything is a neat concept though.

Look at the Porter stemming algorithm for example. It simply does not decompose into something that is easy to put names on.

> 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

Looks fine to me.

(comment deleted)
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.
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.

I don't recall much jumping put and in.

Any way to do that in Python? Basically an anonymous function across multiple lines, which can be collapsed in the IDE view?
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
That’s misleading.

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!

> the code can't be read top-to-bottom

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 :-)

I agree, my thoughts were along this line especially when I noticed the 'magic' number -3.
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.

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.

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.

> 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.
Err - OK - my google fu is failing me. What is MORF (I assume it's some kind of acronym for refactoring ...)
It's defined in the article, the author seems to have invented it.
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?
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

I understand the idea, but it requires:

* 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.
(comment deleted)
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.

[1]: https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction

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.

This style is increasingly common in Swift, especially for Combine pipelines and SwiftUI modifiers.
D does too:

    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);
    }
(comment deleted)
Ruby maps are so ugly.

  .map { |key_value|
  key_value.partition('=')[-1] }
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.

Parentheses as pipes is what you do when you’ve backed yourself into a corner.

Ruby has decided (for some reason?) to use two ways to declare.

Function(param, param)

and

{ |param, param| }

why? What is a good reason for this?

It would be an anti pattern in design, not sure why Ruby gets a special hall pass.

   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.

You might as well have leaned on the keyboard, it wouldn’t look much different. This is not readable code.
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.

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'm curious , can you show an example of how to achieve the same in another language that you will consider pretty?
(comment deleted)
Sub routines are underused. Make it a function right where it is?
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.
I don't know, it doesn't seem very far form the python version:

    values = (
        key_value.partition('=')[-1]
        for key_value in 
        s.partition('?')[-1].split('&')
    )
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.
I build most of my classes that way in python.

    class Motor:
        def __init__(self):
            self.max_speed = 10.0
            self.clockwise = True
            self.controller = Controller()

        def set_max_speed(self, max_speed: float=10.0) -> 'Self':
            self.max_speed = max(0.0, max_speed)
            return self

        def start(self) -> 'Self':
            self.controller.start()
            return self

        etc..

Then you can use it as follows:

    motor = Motor().set_max_speed(5.0).start()
There is nothing stopping you from building iterators that work the same way
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.
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:

    s.split('?')[1]
     .split('&')[-3:]
     .map(lambda x: x.split('=')[1])
Unfortunately, that's not how Pythons map(), len() and such were designed.
Idiomatic Python wouldn't use a map here, but a generator expression:

    (x.split('=')[1] for x in s.split('?')[1].split('&')[-3:])
Removing the lambda cuts down on the noise considerably.

And honestly, with this many splits with fixed indexes, I'd probably use a regex. Now there's a dense language for you.

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.

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.
(comment deleted)