Hacker News new | past | comments | ask | show | jobs | submit login

Temporary variables are often tedious? I have found that well named temporary variables are the only clear way to comment code without actually writing the comment. The version with temporary variables is much easier to understand without having to read the rest of the code.



Exactly. As the proposal contemplates this alternative, it claims:

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


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

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.


If names are the source of crisis, wouldn’t it be better to define temporary variables without names?

  var [$1, $2] = foo(bar(envars))
  console.log(chalk.bold($2), $1)
Job done, no plumbing needed. Has the same level of semantics as %.


and yet looking through code from the place you work I see something like this

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


You've confused method chaining and nesting. The proposal itself says that method chaining is easier to read, but limited in applicability, while it says deep nesting is hard to read. The argument against the proposal by the GP comments is that temporary variables make deep nesting easier to read and do it better than pipes would.


Thanks for taking the time to look and reply.

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?


|> is for functions the same thing that . is for methods

If you program in object oriented style then . is mostly all you need.

If you program in functional style you could really use |>


I like that the expected variable name has a typo.

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


fieldWithHashMarksUnesacped

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


This. Temporary variables are the way to go for deconstructing a complex expression like this. Everything is more readable when you put the results of an expression with two to four terms in a well-named variable. Trying to put everything into one giant closed-form expression feels clever and smart, but it's really just getting in the way of the next poor sucker who needs to understand what you were doing.

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.


Temporary variable make debugging easier too.


Agreed. It feels great to chain stuff or nest function calls, but then I hate myself when it comes to debugging.


Pipe is a tool for dealing with batching too.


I wish I could triple-upvote this.


When there are a few they can be really great. But if you need to accurately name every single intermediate thing they can become visual noise that hides what happens.


I struggle to think of real-world examples where I've just needed to chain and chain and chain values of different types more than a handful of times. The claimed need for the pipe operator is this construction:

    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.


Imagine that you asked someone the question "How do you make a cake?" Which response would be clearer?

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.


Right, it works for your analogy.

Going back to the concrete scenario GP presented, naming things makes it much clearer to me.


In fact, I'm so fanatical about naming things, I'd probably give the two magic numbers and the return value names as well:

    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.


Should make it an async function, and await the bake step. ;-)


Sometimes intermediate values either don't have domain specific meanings or the meaning is obvious from the function name that returns this temporary value.

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();


> Sometimes intermediate values either don't have specific meanings or the meaning is obvious from the function name that returns this temporary value.

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.


> But even granting that this is true: congrats, you've just found the worst part of giving these intermediate steps a name!

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?


Also the second example is easier to manipulate. You can hack in branches, logging etc. during development. I'm also not sure how the proposal tries to solve the problem that we can't easily pluck out members from an object in the first example. Will people just write something like `get(obj, "member")`? Or maybe they thought about this?


How about

    function bakeCake() {
      return do(
        () => gatherIngredients(),
        ingredients => mix(ingredients),
        batter => pour(batter, pan),
        batterInPan => bake(batterInPan, 350, 45),
        () => coolOff(bakedInPan),
        cooledCake => separateFromPan(cooledCake)
      );
    }


...which is just

   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
      );
    }


What this reminds me is of those hierarchies Cat extends Animal... In these simple "real-world-inspired" examples it seems to make sense, but in programming I'd say a lot of times there's simply no good name for the intermediate steps.


In general, I think that when one does that, the code smell one is smelling isn't "This language isn't expressive enough; I need a third way to describe calling a function." It's "What I'm doing is actually complicated and I need to switch to describing it with a DSL, not adding more layers of frosting on this three-layer cake."


All I can figure is the people who keep pushing this sort of stuff in JS have very different problems than I do, if they think this will improve things rather than making them worse.

... I further suspect that their problems are mostly self-inflicted, but maybe I'm wrong about that.


Thank you.

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

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.


The version with temp variables is also easier to debug.


My JS code looks exactly like you describe. Just a bunch of const (rarely let) statements with descriptive, short names. It's not tedious at all, just a little verbose. But JS is already a language that is relatively compact so it doesn't really matter.


I think that depends on the context. Are all intermediate results of the application of multiple procedures relevant and need a name? Or are we only interested in the result after applying all the procedures? Why polute our namespace with names, which are never again used, except for the next step of the pipeline? Then in other cases one does need some intermediate results.


Quite often I rewrite the code from

```

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.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: