"While it's useful to have it split up into logically discrete chunks with sensible naming these 2-4 line functions far increase the mental load of the code."
I don't do this a lot, but in those cases where I'm tempted to pull out "helper functions" that I just call once, simply so I can give them a name, I often use blocks instead. That is, instead of:
func DoTheThing(...) {
x = doTheFirstPart(...)
y = doTheSecondPart(...)
return doTheThirdPart(x, y)
}
func doTheFirstPart(...) { ... }
func doTheSecondPart(...) { ... }
func doTheThirdPart(...) { ... }
I'll do:
func DoTheThing(...) {
// Do the first part
var resultType1 x
{
// basically the contents of doTheFirstPart here
}
// Do the second part
var resultType2 y
{
// basically the contents of doTheSecondPart here
}
// Do the third part
{
// doTheThirdPart here
return final result
}
}
If you're literally going to call the function only once, and it just disrupts the flow, this has a number of advantages. In most brace-using languages, the braces will confine a scope just like a function would have, so you still have clear specification of what can flow out of the brace functions via the previous variable declarations. (They can stack up a bit if you have a lot of these sections but I've not had too much trouble with that.) If you do need to pull it out into a function later, it's a fairly mechanical process instead of a surgical process, because you're already 90% of the way there. But if you never need to do that, you retain the ability to simply read through a function and see everything it's doing.
I've noticed a lot of my "payload" functions sometimes end up long, but if you look carefully, you'll see I'm still using a lot of tools for scoping and simplification to confine action-at-a-distance within the function itself.
You do get a couple of weird looks and possibly some weird review comments if you use this; people are not used to seeing scopes used solely for isolation like this. I've had people ask me whether this is a syntax error or not in the review. But usually once you explain yourself it passes through.
This seems to mostly end up in my testing code, where I may need a chunk to set up the environment for a particular chunk, and then may have some tests I want mostly isolated from each other, but to use the same environment, or in a recent case, I had a chunk that setup the unique environment for the test, the code under test was basically "testEnvironment.DoIt(...)", and then I had half-a-dozen blocks verifying that the DoIt call had done all the things it was supposed to do and had all the exact side effects it was supposed to have, one class of them at a time, confined in braces so it's syntactically enforced they can't have leaking values. (This was an integration-level test.)
There are definitely other cases where simply naming things and breaking them into functions can be helpful, so you end up with an "orchestration" function that reads cleanly, and all the little bits also read cleanly. I do that plenty too.