-
Notifications
You must be signed in to change notification settings - Fork 108
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added value of hack-style proposal against temporary variables #200
Comments
Emphasis mine. This is the main issue with your suggestion, as it's highly unlikely a majority of users would agree on this style vs a pipeline operator. Much of your argument around the aesthetics of Lastly I'd add that while the "naming things is hard" argument might be the driving one behind Hack-style specifically, it is not the primary benefit of the pipeline operator itself. That would be its more linear and less nested ordering of function calls. |
NOTE: a similar issue was discussed previously #173 with some interesting pros and cons, though back then we compared it to both Hack-style and F#-style pipelines. I agree with the idea that assignment sequence seriously questions added value of Hack-style pipelines in the language. @citycide, I think the agreement needs be achieved only inside a single codebase. |
If I understand you correctly, this means people would not converge on a single variable name for this, so let's force one upon them. Sure, but that still does not require an operator. You could basically achieve that by having
Isn't agreeing on (and changing) linter rules easier than introducing new syntax to the language itself?
But it is (according to the README of this proposal) the main benefit over temporary variables. In other words, you do get the more linear and less nested ordering of function calls with assignment sequences as well (that is without the pipeline operator). |
It’s definitely still very different from temporary variables. Having to name each step, or having to reuse an identifier, are problems; reusing a syntactic token is not. |
Normally you don't have to. In fact, the hack-style pipeline proposal simply chooses a name for you for resolving that issue and nothing more. We could agree on the such a token without new syntax being added to the language. Perhaps this example would help me better clarify myself: What if we just got envvars;
Object.keys(^);
^.map(envar =>
`${envar}=${envars[envar]}`);
^.join(' ');
`$ ${^}`;
chalk.dim(^, 'node', args.join(' '));
console.log(^); Of course this code is harder to read, because the value of
I think I am missing something here. How is re-using |
It’s entirely different, because i can’t make a variable named ^. Javascript devs know what valid variable names are. |
These fake-pipeline reassignment chains come with their own overhead, so claiming they are somehow simpler is a dubious argument. @theScottyJam previously made good points on this topic so I will defer to what was said there already.
It isn't even the first benefit listed there — there are three listed before it that you're skipping over:
Agreed, and so do engines, compilers, and linters, some of which could raise errors or similarly guide devs to use a feature properly. Reusing a variable is more complex and error prone in comparison. |
@citycide I am not. I rather feel I am unable to convey properly what I am talking about. Let me try one more time:
To put this schematically: we want to achieve |
This is indeed a good point (basically, increased coding safety). However, I cannot help but think this is something you could achieve with some linting rules instead of new syntax, similar to how the main power of |
It would be poor language design to abdicate responsibility for safety and push it onto the ecosystem, when it's achievable to avoid doing so. |
i think README.md should be updated with |
I agree with the sentiment generally, though I feel in JS space people are super used to picking their own safety features (and thats why Prettier and TypeScript are super popular). I cannot argue one way or another about where the line should be drawn. Hack-style pipelines are super close to assignment sequences using a token such as Regardless, if safety is an important benefit of hack-style pipeline pattern over using temporary variables, I suspect it at least should be mentioned in the README. P.S. The primary actually used-in-practice pattern referenced for this proposal is jQuery or Mocha style method chaining. These patterns differ from hack-style in that they completely eliminate the need for a placeholder to begin with. I am all for standardizing and further facilitating commonly used design patterns in the language itself, but hack-style pipelining doesn't strike me as such a pattern. |
@kaizhu256 so are most JS styleguides and best practices - variables shouldn't be reassigned. @loreanvictor even if an identifier was viable (it is not), |
Yeah I am using it as a mere example, not saying that this particular token has any benefits over ^ (though it visually does work better for me at least, and it is not an operator) |
at the macro/ux level of js-programming, temp-variables are reassigned all the time as placeholders for data waiting to be serialized/message-passed. this proposal feels like an acknowledgement of above reality, but allowing ppl w/ dogmatic views like |
@kaizhu256 i hear that that’s your experience, but in mine, in those exact use cases, variables are never reassigned. |
I don't think passing judgement on people participating in the discussion (such as calling their view dogmatic) helps advancing the discussion. |
but i do feel this proposal is political and advanced by ppl with certain dogmas on programming (static-typing and immutability of variables), but want to break that dogma in real-world programming, without having to admit to breaking it. and increasing the language complexity for the rest of us. |
@kaizhu256 - careful - these people have very good reasons to believe the way they do about variable reassignment, just like you feel you have good reasons to believe variable reassignment is a good thing. An important part of language design is trying to figure out what patterns are good, what patterns are bad, and encouraging the good ones. They're currently under the belief (and so am I), that variable reassignment is generally a thing that should be avoided. Of course, these beliefs can be challenged, opposing scenarios can be brought up, and things can change. But simply calling a particular viewpoint "dogmatic" is just hurtful and unhelpful. I wish, in the programming community as a whole, we would stop using that term. It never leads to good things. |
"Prefer |
Additionally, treating the placeholder as a "reassigned variable" is a mistake. It's not a general-purpose variable; it's explicitly the result of the LHS of the operator. Its value is narrowly defined, its scope is narrowly defined, and thus its purpose is narrowly defined. The placeholder is thus more akin to block scoping than temp variables, a feature I hope we can all agree is a significant improvement to the language. |
@loreanvictor - I think I'm following your argument, and I think what you're basically showing is that the README's argument for "variable naming" being the reason pipes are preferred over reassignment is simply not a great argument and perhaps needs to be updated. What you gave was pretty solid evidence that that argument by itself does not create enough merit to introduce a pipeline operator. Security has been alternative reasoning that's been discussed around here. I personally view it as a readability thing more than anything - which I tried to explain thoroughly in the post that already got linked to over here. It's really easy to see a pipeline and know at a glance what's going on. It's much harder to look as a bunch of variable reassignments and know what's happening - variable reassignments have a lot more power to them, and can cause a lot of other things to happen that you might not expect. Pipelines, on the other hand, constrain the way you write your logic to follow a specific structure that can be understood at a glance and can't ever be deviated from. I also believe that an important part of language design is to encourage good practices, and provide ways to make them easy to follow. Structuring code in a pipeline fashion is generally a good practice - code structured that way is generally more readable. For example, a function with a single pipeline is very easy to follow, and there's not a lot of stuff you have to keep in your head to folllow it - you just need to know how the previous step in the pipeline behaves in order to understand the next step. On the other hand, a function full of individual statements and many different variable names is much harder to follow. You have to keep in your mind a much bigger context - what the values of all of those different variables are at different points in time. The dependencies between one line and its predecessor lines are also much more complicated, due to the fact that any line can reference a variable created from any of the previous lines. There's also the fact that it's very, very easy to use side-effect inducing functions outside a pipeline. Inside a pipeline, programmers are naturally going to use pure-er functions, because you're forced to do something with whatever got returned from that function in the next stage of your pipeline. Using a temporary variable puts you into this kind of pattern, but doesn't force anything, and doesn't make it easy to see at a glance that this pattern is being followed. |
Speculating on motives is rarely productive or appropriate. In this case, it's wrong - I'm a primary champion, and don't feel either of those are particularly important. (But I recognize that it does affect a number of people, so it's not meaningless and should be considered.) My counter-argument to this thread is simply: if people could do this today and use it to simplify their code in a meaningful way, why aren't they doing it? As far as I'm aware, this style of repeated re-assignment does not show up in any meaningful amount in the wild. Instead, they write difficult to read, heavily-nested code, or use uber-objects, or There are a few possible conclusion to be drawn from that:
There's also a practical issue - re-assignment like this means that each pipeline step is seeing the exact same variable, so any closures over that variable will see the updated version from the end of the "pipeline", rather than the version that existed during their step. This is the same sort of footgun that used to plague callbacks created in <!DOCTYPE html>
<script>
var _ = 1;
_= (setTimeout(()=>console.log(_), 1), _),
_= 2*_,
_
// logs 2
</script> The only way around this with named variables is to do what the example code in the README does, and use a unique name at each step. But that means you either have long, meaningful names, or short meaningless names that all look very similar, and this invokes the other issue with constantly naming steps - it makes it hard to determine at a glance what the scope of each variable is. Each one could be used in any of the succeeding steps; there's no guarantee that they're single-use in just the next line. Summarizing my points:
|
I think the closure limitation is the clincher, really, and avoids the subjective arguments entirely. |
With all due respect: P.S: I wish we could see videos and/or notes from the September meeting, before things moved so fast in this repo. The lack of communication of the reasoning behind Hack over F# decision (before closing issues and updating readme & specs) doesn't let us fully and properly participate in the discussions. |
Your argument in this thread was not over pipeline syntaxes, but over the need for a pipe operator at all. It applies equally well to any pipeline syntax. |
I actually have to disagree. the argument I made was specifically around the hack-style pipeline. the F#-style pipeline doesn't have a token representing the preceding value, so this argument doesn't apply to it. |
I feel my original question was "being too wordy doesn't seem like enough of an argument against temporary variables since we can make it equally wordy, are there any other reasons to prefer hack-style pipelines over temporary variables?" and we already have answers for that (basically code safety by avoiding variable reassignment since ^ is not a reassignable variable). I would recommend on formulating a real world-ish example highlighting that code safety difference, creating a PR to include this additional reasoning in the README as well, and then if there are arguments against those additional reasons, we can discuss them in separate issues afterwards (basically after they are well formulated and added as proper reasons for hack-style pipelines over temporary variables). |
Hack-style and F#-style pipelines are (with some caveats) equivalent in power; anything you can do with one you can do with the other. Temp-var reassignment is an argument against the need for a pipe operator at all; it happens to resemble Hack-style pipelines more closely, but the argument for/against it is has zero details specific to Hack-style pipelines. The presence or absence of a placeholder token isn't significant; F#-pipeline absolutely does as well when you're using arrow functions (the arrow-func's argument). As far as I can tell, the argument for temp-var reassignment doesn't hinge on the alternative being point-free or not, either. It's possible I'm missing something - can you elaborate on why you think temp-var reassignment is an argument against one pipe operator syntax in particular? |
Good point @tabatkins I guess in other words, these are all equivalent:
Given a properly structured temp-var pipeline, it's easy enough to do a direct conversion into either hack-style or F#-style. If hack-style pipes are seen as unecessary, because a temp var pipeline is a good enough replacement, then F#-style pipes should also be seen as unnecessary, as the differences between hack and F#-style are really quite minor (in F#, you use a function literal when you need topic-like behavior, in hack, you use a "(^)" when you're just wanting to use a single-param function - that's about it) |
Same; they are quite different - if for no other reason then you can use the placeholder inside a closure, but if you use a temp var inside a closure you get silent bugs (the kind we're all very familiar with from decades of functions created inside for/for-in loops) |
@ljharb @mAAdhaTTah I fully agree with these points. I tried to mention and elaborate on this difference in the Temp-var Reassignment is Unsafe section, with an example of such a silent bug that would be avoided with Hack-style pipelines. Did I miss anything? Or perhaps I can improve some wording somewhere for further clarity? |
This is the statement I specifically object to in the summary, under "Mutable variables are harder to reason about". I disagree that the topic token is similar to a mutable variable, and if this summary is to be complete, it should include a explanation as to why that's the case. |
@mAAdhaTTah What I meant is that to be able to reason about the |
I don't think the third line is a good example in any style, tbh, and wouldn't be constructing arguments around it. In my original writeup, my suggestion was to write a function in all styles.
Yes, this is the claim I disagree with, because |
Do you agree with the fact that await chromeClient.rpc("Page.captureScreenshot", { format: "png" })
|> (assertOrThrow(!^.exceptionDetails, ^.exceptionDetails), ^)
|> await fs.writeFile("screenshot.png", Buffer.from(^.data, "base64")); P.S. I do understand the scope difference and agree on that point, that is simply not the point I'm trying to make. I just find this code hard to read because I cannot map |
Yes, but because that |
Good point. Can you also provide an example? All that comes to my mind are dirty in-place assignment expressions. |
Your argument is that "^" is difficult because you need to look to the previous line to know what it means. Couldn't a very similar argument be said about F#-style? What the function gets called with can't be determined unless you look at the previous line? ok - maybe that's a weak rebuttal, but it's my thinking. on a different note, I agree that when you format your code like this, it's very easy to tell that you're basically just doing a fake pipeline using existing tools.
It gets much harder to follow that if code like this starts popping up:
And yes, that's a variation of your "dirty in-place assignment expression" example. |
I think the example we've been discussing works fine. The original code snippet has |
Yeah me neither, which is why it is a bit hard for me to also come up with similar examples. However, when contemplating potential increase in code complexity I would need to debug, I find it healthy to look at code outside of my own / preferred / linter-wouldn't-go-mad-at styles as well. Anyways I updated the summary to reflect your point. |
@theScottyJam veering a bit off-topic, but, The steps of the F#-style pipeline are fully independent from each other, so I don't need to read the previous step for reasoning about the next. However, with Hack-style pipelines, they have a dependency on each other in form of the However this is of course not a completely fair comparison, since in F#-style pipelines each step represents a higher-order construct (it is an expression resolving to another function, instead of a plain value), while in Hack-style pipelines each step resolves into a plain value. So if someone is not accustomed to reasoning about higher-order functions / expressions, perhaps they might find reading Hack-style pipelines easier still. |
@loreanvictor that's not accurate. Every step in either pipeline depends on the output from the previous one. In F#, that dependence is implicit and invisible, but conventionally assumed in FP circles; in Hack that dependence is explicit with the |
Yeah I tried to elaborate on this in the second paragraph as well. While this is true, I have (personally) got used to not trying to fully resolve the higher-order expressions of F# pipelines, which allows me to ignore the dependency, while in Hack-style, I feel kind of forced to deal with it heads on. Again, this should not be read as a strict pro for F# vs Hack, since it assumes being comfortable with reasoning about higher-order stuff, which statistically might not be a correct assumption (I highly suspect this is one of the main reasons people try to avoid RxJS). |
Closing this issue since the all the advantages of Hack pipelines over mutable temporary variables are now listed in the README (see this and this section). I suspect further discussion on these points can be made in separate issues in a more focused manner. |
FWIW, the section on "pipelines resolve to expressions" hasn't considered that you can also have reassignments in expressions. return (
<ul>
{
values
|> Object.keys(^)
|> [...Array.from(new Set(^))]
|> ^.map(envar => (
<li onClick={() => doStuff(values)}>{envar}</li>
))
}
</ul>
) can easily become let $;
return (
<ul>
{
($= values,
$= Object.keys($),
$= [...Array.from(new Set($))],
$= $.map(envar => (
<li onClick={() => doStuff(values)}>{envar}</li>
)))
}
</ul>
) Again, not suggesting that this is a good practice or that Hack doesn't make this slightly better, but it kind of invalidates that point. Should I raise a PR amending that? |
Ah, that's true. That'll let you do it in a lot of expressions. But not all.
Nested pipelines would also have an issue. Well, I guess you could get around these issues if you used a different variable name for the topic variable. But yeah, this quickly becomes pretty gross. |
@voliva because capturing |
Another pull request to improve the documentation’s examples would be welcome. |
Please, I explicitly said
Of course it's not to be recommended. Of course it's dangerous. Of course it's buggy. Of course Hack has an advantage over reassigning. I'm just pointing out that from this discussion, something was added on the README which isn't fully true. |
It would be a very bad idea for the readme to imply that reassigning temp vars is a viable alternative - it’s far better to omit it. I’m probably missing something tho - if you think the readme can be improved, im sure the champions would appreciate a pull request. |
TLDR
The current argument for hack-style proposal against temporary variables is:
In the hack-style proposal, we still have a variable representing the intermediate values. However, we have overcome these two issues simply by giving it a consistent, short and meaningless name, i.e.
^
.This means the argument currently mentioned against temporary variables IS NOT resolved by the pipeline operator itself, it is simply resolved by picking a consistent, short and meaningless placeholder token. In other words, we DO NOT need a new operator (with new syntax support) for resolving these issues, we could simply go the temporary, single-use variables route and just drop the need to pick unique and meaningful names for them, and basically achieve the same thing.
The main other added value that I can think of for hack-style pipelines vs temporary variables is the fact that the whole pipeline is a singular expression resolving to a value, which is not true for a sequence of assignment statements. It is notable that the real gain here is also only in short and simple pipelines.
Explanation
This is the example code mentioned against temporary variables:
Its issues are resolved by the pipeline operator like this:
However, we do not need the operator itself for gaining the benefits here. If we were to agree on using meaningless variable names for intermediate values in such chains, then we could simply rewrite the original code like the following:
Note that:
let _=
overhead, which seems ignorable (since it doesn't scale with the chain size / complexity)_=
vs|>
is debatable. While|>
seems visually more close to the concept of pipelines,_=
benefits from familiarity (people already know what it means and don't need to learn a new concept)_
is a better placeholder character than^
, both visually, and because^
is already an operator in the language as well (though one that is not that commonly used)But in real life, we tend to use meaningful names for variables
With the hack-style proposal, we are already conceding on that front, as the placeholder variable will be a meaningless variable name. Besides some visual preference and some potential confusion with existing operators, there isn't much difference between using
^
to represent the intermediate value and using_
. This means all the issues that would rise from NOT using meaningful names for intermediate variables would also apply to current hack-style proposal, and this cannot be used as an argument for the hack-style proposal against temporary variables.Statements vs Expressions
One argument that could be made in favor of hack-style pipelines vs temporary variables is that a pipeline expression is an expression, resolving to a singular value. This means you could do things like this:
The last case can easily be re-written as the following:
The third case is arguably bad practice, as making the flow of execution consistent with reading direction is one of the main purposes of pipelines to begin with, which is violated here. It can also be easily re-written like this:
As for the second case, with longer pipelines that have more complex expressions. This means for a typical clean code, each step in the pipeline would occupy its own line and the overall overhead of having a function with a return statement would be minimized. A similar argument can be made for the first case, were the assignment would happen at the end of the sequence instead of at the beginning, without much overhead between the two cases.
None of these arguments hold for short pipelines with simplistic expressions, where there might be a value in writing the whole pipeline in a single line / expression. In these cases though, the overall added value of pipelining is marginalized, and it might not be as clear whether the overhead of adding new syntax to the language would be worth such marginalized gains or not.
The text was updated successfully, but these errors were encountered: