> But there are reasons why we encounter deeply nested expressions in each other’s code all the time in the real world, rather than lines of temporary variables.
And the reason it gives is:
> It is often simply too tedious and wordy to write code with a long sequence of temporary, single-use variables.
Sorry, but...that's the job? If naming things is too hard and tedious, you don't have to do it, I guess, but you've chosen a path of programming where you don't care about readability and maintainability of the codebase into the future. I don't think the pipe operator magically rescues the readability of code of this nature.
The tedium of coming up with a name is a forcing function for the author's brain to think about what this thing really represents. It clarifies for future readers what to expect this data to be. It lets your brain forget about the implementation of the logic that came up with the variable, so as you continue reading through the rest of the code your brain has a placeholder for the idea of "the envVar string" and can reason about how to treat it.
The proposal continues:
> If naming is one of the most difficult tasks in programming, then programmers will inevitably avoid naming variables when they perceive their benefit to be relatively small.
Programmers who perceive the benefit of naming variables to be relatively small need to be taught the value of a good name, and the danger of not having a good name, not given a new bit of syntax to help them avoid the naming process altogether.
The aphorism "There are two hard problems in computer science: cache invalidation, and naming things." is not an argument to never cache and never name things. That's mostly what we software folks spend our time doing, in one way or another.
let field = ve.instanceContext.replace(/(#\/)|(#)/ig, "").replace(/\//g, ".")
Which you apparently claim should be const fieldWithHashMarksUnesacped = ve.instanceContext.replace(/(#\/)|(#)/ig, "");
const field = fieldWithHashMarksUnesacped.replace(/\//g, ".")
https://github.com/mirusresearch/firehoser/blob/46e4b0cab9a2...and this
return moment(input).utc().format('YYYY-MM-DD HH:mm:ss')
Which apparently you believe should be const inputAsMoment = moment(input);
const inputConvertedToUTC = inputAsMoment.utc()
return inputConvertedToUTC.format('YYYY-MM-DD HH:mm:ss')In your first find, yes, your modification helps me understand that code much more quickly. Especially since I haven't looked at this code in several years.
In that case, patches welcome!
In your second case, as the sibling comment explained, I'm not opposed to chaining in all cases. But if the pipe operator is being proposed to deal with this situation, I'm saying the juice isn't worth the squeeze. New syntax in a language needs to pull its weight. What is this syntax adding that wasn't possible before? In this case, a big part of the proposal's claim is that this sequential processing/chaining is common (otherwise, why do we care?), confusing (the nested case I agree is hard-to-read, and so would be reluctant to write), or tedious (because coming up with temporary variable names is ostensibly hard).
I'm arguing against that last case. It's not that hard, it frequently improves the code, and if you find cases where that's not true (as you did with the `moment` example above) the pipe operator doesn't offer any additional clarity.
Put another way, if the pipe operator existed in JS, would you write that moment example as this?
return moment(input)
|> %.utc()
|> %.format('YYYY-MM-DD HH:mm:ss');
And would you argue that it's a significant improvement to the expressiveness of the language that you did?If you program in object oriented style then . is mostly all you need.
If you program in functional style you could really use |>
Those typos leak out to calling code and it's hilarious when the typo is there 10 years later once all the original systems have been turned off
The code removes all “#/“ (or just “#” if a slash isn’t there). After that it replaces slashes with dots. How on earth is that “hash marks unescaped”?
Sure, it can’t be completely eliminated, but why not do less of a thing that’s hard, when it can be avoided?
Values have a “name”, whether it’s a variable ‘keysAsString’ or the expression ‘keys.join(' ')’. The problem with keysAsString is that you have to type it twice, once to define it and again to use it. It’s also less exact, because it’s a human-only name, not one that has a precise meaning according to the rules of the language. (E.g. a reader might wonder what the separator between the keys was - if you don’t store it in a variable, then the .join “name” tells you precisely right at the site it’s used.) Making the variable name more precise implies more tedium in the writing and reading.
If the value is used twice or more, I would usually say storing it in a well-named variable is preferable, but if it’s cheap or optimizable by the compiler I might still argue for the expression.
This may be a irreconcilable split between different types of thinkers, perhaps between verbal and abstract.
var [$1, $2] = foo(bar(envars))
console.log(chalk.bold($2), $1)
Job done, no plumbing needed. Has the same level of semantics as %.This works the way human cognition does, by batching. The way humans can fit more items in short-term working memory is to batch up related concepts into one item. This is how chess masters do it - they don't see a piece and look individually at each square it is attacking, they see the entire set of attacked squares as one item. This is why "correct horse battery staple" passwording works - the human doesn't remember twenty-eight individual characters, they remember four words.
Temporary variables follow how human cognition works, particularly when the reader is going to be somebody else's cognition who didn't go through the process of writing it.
function bakeCake() {
return separateFromPan(coolOff(bake(pour(mix(gatherIngredients(), bowl), pan), 350, 45), 30));
}
The piped code looks like: function bakeCake() {
return gatherIngredients()
|> mix(%, bowl)
|> pour(%, pan)
|> bake(%, 350, 45)
|> coolOff(%)
|> separateFromPan(%)
;
Which is... fine? It certainly looks better than the mess we started with, but adding names here only helps clarify each step. function bakeCake() {
const ingredients = gatherIngredients();
const batter = mix(ingredients);
const batterInPan = pour(batter, pan);
const bakedCake = bake(batterInPan, 350, 45);
const cooledCake = coolOff(bakedInPan);
return separateFromPan(cooledCake);
}
Even if you consider the `const` to be visual noise, the names are useful. At any point you can understand the goal of the code on the right-hand side by looking at the name of the variable on the left-hand side. You can also visually scan the right-hand side and see the processing steps. You can also introduce new steps to the control flow at any point and understand what the data should look like both before and after your new step.I agree that the the control flow is more clearly elucidated in the pipe operator example, but it tosses away useful information about the state that the named variables contain. It also introduces two new syntactical concepts for your brain to interpret (the pipe operator and the value placeholder). I contend the cognitive load is no greater in the example with names, and the maintainability is greatly improved.
If you have an example where there are dozens of steps to the control flow with no break, I'd be really curious to see it.
function bakeCake() {
return do(
() => gatherIngredients(),
ingredients => mix(ingredients),
batter => pour(batter, pan),
batterInPan => bake(batterInPan, 350, 45),
() => coolOff(bakedInPan),
cooledCake => separateFromPan(cooledCake)
);
} function bakeCake() {
const ingredients = gatherIngredients();
const batter = mix(ingredients);
const batterInPan = pour(batter, pan);
bake(batternInPan, 350, 45); // this is an in-place modifying function, I guess
const cooledCake = coolOff(bakedInPan);
return separateFromPan(cooledCake);
}
...but with an extra `do(...)` wrapper?It could at least be
function bakeCake() {
return do(
gatherIngredients,
mix,
batter => pour(batter, pan),
batterInPan => bake(batterInPan, 350, 45),
coolOff,
separateFromPan
);
}
Although if we had function currying, the convention in ML languages is to put the most-commonly-piped-in param last for these functions: function bakeCake() {
return do(
gatherIngredients,
mix,
pour(pan), // assuming that pour(pan) returns a function that pours something into that pan
bake(350, 45), // assuming that bake(temp, minutes) returns a function that bakes something at that temperature for that time
coolOff,
separateFromPan
);
} function bakeCake() {
const bakeTemperature = 450;
const bakeTime = 45; // minutes
// ...
const bakedCake = bake(batterInPan, bakeTemperature, bakeTime);
// ...
const finishedCake = separateFromPan(cooledCake);
return finishedCake;
}
And I'd not look at a code review which quibbled about the particular names I chose as being a waste of time either. Time spent in naming things well is the opposite of technical debt, it's technical investment. It pays dividends down the road. It increases velocity. It makes refactoring easier. It improves debuggability. It makes unit tests easier to see.Then naming it is just noise.
If your bake() function was rather named createBakedCake() than naming returned value bakedCake just increses reader fatigue through repetition.
Same way
Random random = new Random();
in C# is worse than
var random = Random();
I don't necessarily disagree with this. But even granting that this is true: congrats, you've just found the worst part of giving these intermediate steps a name! Like, that's the worst case example of the cost side of the tradeoff we're discussing here. And it's not that big a cost! Like, of all the code you write, how much of it fits this case? Where you're writing a function where there's a lot of sequential processing steps in a row with no other logic between the steps AND the intermediate state doesn't have any particular meaning?
In that worst case, you have a little extra information available (like your Random random = new Random()) example that your eyes need to glide past.
I would wager your brain is more used to scanning your eyes past unnecessary information and can do that with less effort and attention than it can either:
- bounce back and forth between the chained function calls of the original nested example.
- synthesize the type and expectations of the intermediate value at any arbitrary point in the piped call chain.
That last thing is the big cost of not naming things. In order to figure out what the value should look like at step 4, you have to work backwards through steps 1-3 again. And you have to do that any time you are debugging, refactoring, unit testing, adding new steps, removing existing steps, etc.And the work to come up with "obvious" names isn't hard. Start with the easy name:
batterInPan = pour(batter, pan)
And if the name batterInPan never gets any better and never really helps anyone read or debug or refactor or unit test this code, then in that sense, I guess it's a "waste". I just claim that this case is far less common in the real world and far less costly than having to untangle a mess of unnamed nested or chained call values.Or maybe you want to just start with the unnamed nested or chained calls, and when you need to read or debug or refactor or test your code you pay the "naming things" price tag at that point. That's often the first thing I do when I come across code with a dearth of names, I just give everything a boring, uncreative temporary name, and then I can do whatever work I showed up to this code to do. It's not ideal, but it's better than every JS library sprinkling a new bit of syntax in just so they can avoid giving their variables names and can use an overloaded modulo operator instead.
Yes. But given that people would usually put you on a stake for naming function bake() because it doesn't tell anything about what the function expects or returns and bare minimum about what it does, this use case scenario is what happens very often, because naming your function in a very informative manner is very important because they are a part of the API.
If you really have functions like bake() or pour() in your code esp in weakly typed language then for the love of God, yes, please name the variables that you pass there and get from them always and as verbosely as possible.
Don't get me wrong, I'm very fond of naming intermediate things too. And with helpful IDE it can even tell you the types of intermediate things so you can better understand the transformations that the data undergoes as it flows through the pipeline.
But sometimes type, that IDE could show also automatically in |> syntax is even more important than the name for understanding. VS Code does something like that for Rust for chaining method calls with a dot. Once you split dot-chain into multiple lines it shows you what is the type of the value produced in each line.
My personal objection to naming temporary values too much in a pipeline is that it obscures distinction between what's processed and what are just options of the each processing step. But I suppose you might keep track of it by prefixing names of temporary values with something.
> Or maybe you want to just start with the unnamed nested or chained calls, and when you need to read or debug or refactor or test your code you pay the "naming things" price tag at that point.
Yeah, that's usually what I do. I start with chains and split them and pay for the names as I go.
> That's often the first thing I do when I come across code with a dearth of names, I just give everything a boring, uncreative temporary name, and then I can do whatever work I showed up to this code to do.
I'm also splitting and naming stuff in that case and checking types along the way. But I prefer that to encountering the code named verbosely and wrongly. Then I need to get rid of the names first to see the flow then split it again sensibly. Of course I don't usually commit those changes in shared environments. Only in owned, inherited ones or if the point of my change is to refactor.
Granted that chaining class member accessor mostly covers up this problem of naming intermediate things if you use classes. That's why we even survived without pipe syntax. But since we would like to move away from classes a bit to explore other paradigms maybe it's time?
1. Gather the ingredients, mix them in a bowl, pour into a pan, bake at 350 degrees for 45 minutes, let it cool off and then separate it from the pan.
2. Get ingredients by gathering the ingredients. Make batter by mixing the ingredients. Make batter in a pan by pouring the batter in a pan. Make a baked cake by baking the batter in the pan at 350 degrees for 45 minutes. Make a cooled cake by cooling the baked cake. Separate it from the pan.
For me personally #1 is more readable because #2 is unnecessarily bloated with redundantly described subjects.
Going back to the concrete scenario GP presented, naming things makes it much clearer to me.
... I further suspect that their problems are mostly self-inflicted, but maybe I'm wrong about that.
I often find flow-crutches like the one described in this proposal more confusing to decipher than plain old fashioned, well thought out code.
Lambda (=>) expressions in C# and closures in JavaScript are others I sometimes find myself pausing at to make sure I'm interpreting correctly.
I always figured it's just because I'm an older programmer and haven't used the new language features enough for them to become intuitive. I do acknowledge there are use cases where they're a perfect fit for the pattern in which you're coding.
But I feel like they're too-often taken as a shortcut to dump a bunch of operations in one place when it would be more readable to structure into well-organized functions that logically group concerns.
It's not that I don't like syntactic sugar to make code more concise, I just think languages need to remain judicious about how many different ways they dole out to accomplish the same task before they start to risk 'rotting their teeth'. Gotta keep striving for elegance - as you renovate over time it can get harder to keep the bar high.
I'm afraid these pipe operators will become like ternaries and will tend to produce "smart" lines of code which are difficult to parse at first glance.
Temporary variables are great for writing obvious code which is trivial to parse when reading.
```
return validate(get_response(value))
```
or
```
value = get_response(value)
value = validate(value)
return value
```
into
```
res = get_response(value)
new_res = validate(res)
return new_res
```
Why? Easier to read and when Sentry throws an error I have each value from the call stack. Much easer to debug. In the example 2 you can accidentally move a line and not notice the error.