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 :-)
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.
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).
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.
"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.
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.
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.
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.
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.
[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'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.
Look at the Porter stemming algorithm for example. It simply does not decompose into something that is easy to put names on.
Looks fine to me.
On the whole I think such functions are valuable. But they do have downsides.
I don't recall much jumping put and in.
.map { | *String* key_value | key_value.partition... }
where String shows up in light grey text indicating that IJ knows `key_value` is a StringDoing 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.
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.
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?
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.
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.
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.
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!.
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.
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
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?
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.
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);
} 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 values = (
key_value.partition('=')[-1]
for key_value in
s.partition('?')[-1].split('&')
)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.
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!
.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)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.
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.
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 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.
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.