The JS one does seem to have some more power than the Elixir one. For instance, in Elixir, if it's a bit kludgy to pipe to a second or third argument. I find this often when I want to insert into a map. You can always define more functions, but otherwise it's annoying, because you end up with something like
computation() |> &(Map.put(my_map, key, &1)).()
That said, with this less-power, you do kind of end up forced to design your functions in a way to where piping makes sense, and IMO it leads to cleaner and more consistent APIs.
That's probably a wise choice. Pipes are great for simple situations, mostly for code clarity. But it can be abused easily, like a hammer seeking nails.
It's similar to await/async, you eventually start designing code that better suits that interface rather than pigeonholing it with complex syntax.
It's nice in Elixir because there's a strong convention that the first argument is where you'd almost always pipe into. The JS proposal is better for retrofitting in a language.
Oh wow, TIL about `Kernel.then/2`. That definitely works around the syntax problem.
An aside, the implementation is kind of amusing. It almost seems unnecessary for this to be a macro but maybe the compiler can optimize this a bit more? I would expect TCO to simplify of my "simple" implementation of
Pipes work really well with named arguments and partial-application. Both functionalities make it easy to get the unary function you want with minimal cruft. Lambdas solve the more complex case.
In Ocaml, you often end up doing things like:
List.create 0 3
|> List.map ~f:(fun x -> x+1)
|> List.fold ~init:0 ~f:(fun acc x -> x+acc)
|> Stdio.print_endline "%d"
I'm ambivalent about adding them to JS however. It's a nice feature but I don't think it works well with the rest of the syntax.
As I mentioned upthread, most Elixir functions are designed to have the thing they operate on as first argument.
computation() |> &(Map.put(my_map, key, &1))
is terrible style when
Map.put(my_map, key, computation())
works just as well and is more readable. It is pretty rare to have a pipeline that needs to insert the value elsewhere than the first position. And please, do not write single element pipelines, I see them far too often from Elixir beginners.
I agree! I even pointed this out in my comment, but perhaps not clearly :)
I think that the way it's done is a net-positive in designing cleaner APIs, but there are times when I've already done a pipeline, and storing the output is just the last step. This last step is just frustratingly, not always possible. I don't think one should do something like the above, it's just what you must resort to if you _did_ want to do it.
The pipe operator in Elixir also benefits from having the subject of most functions in the standard library as first argument. No need for the % hack operator, so you can just do:
This isn't the pipe operator in elixir. It's from some php derivative language that facebook uses and you shouldn't trust it, because nobody could even show in TC39 that it was sound.
They should have stuck with the F# proposal. The hack proposal just takes one more giant step toward turning JS into Perl.
Hack proposal
value |> foo(%) for unary function calls,
value |> foo(1, %) for n-ary function calls,
value |> %.foo() for method calls,
value |> % + 1 for arithmetic,
value |> [%, 0] for array literals,
value |> {foo: %} for object literals,
value |> `${%}` for template literals,
value |> new Foo(%) for constructing objects,
value |> await % for awaiting promises,
value |> (yield %) for yielding generator values,
value |> import(%) for calling function-like keywords,
F# proposal
value |> x=> x.foo() for method calls,
value |> x=> x + 1 for arithmetic,
value |> x=> [x, 0] for array literals,
value |> x=> ({foo: x}) for object literals,
value |> x=> `${x}` for template literals,
value |> x=> new Foo(x) for constructing objects,
value |> x=> import(x) for calling function-like keywords,
F# proposal would make `await` and `yield` into special syntax cases or not allowed.
I'd rather do await/yield the old fashioned way (or slightly complicate the already complex JS syntax rules) than add the weird extra syntax. Arrow functions are elegant and already well-known and well-understood.
Is either proposal compatible with later adding a backward pipe <| operator?
If so I think that should be a consideration, even if in practice there isn't massive value in a backward pipe operator in javascript. It would be a shame to later want to add it and have to add another layer of kludge and messy syntax.
Yeah, F# has a backward pipe operator, and it works like this:
a_value |> (fun a b -> a + b) <| b_value
In F#, this works because of automatic function currying. Not sure how that would apply to Javascript, though. Without function currying, what would this even mean?
a_value |> ((a, b) => a + b) <| b_value
...since there's no currying, you'd just immediately invoke the function that sits "in the middle" with `a` set to `a_value`, but `b` set to `undefined`.
The Hack-style proposal though...I don't think it would work with a backward-pipe operator at all. Not without adding a _second_ special-case symbol, at least.
For the non initiated amongst us who might be confused, F# has first class functions and everything is curried.
The expression you see is evaluated left-to-right. (|>) is a function taking a_value and (fun a b -> a+b) as arguments and applying a_value to the function. This returns a function taking b as an argument (that’s called a partial application). (<|) is once again a function taking the resulting function and b_value as arguments and applying b_value to its first argument which finally returns the wanted results.
The issue with unary function doesn't exist in F# because every functions can be seen as a unary function returning a function taking one less argument thanks to partial application. That's the beauty of currying.
This proposal reminds me of Scala with anonymous parameters '_'. Could I use more than one '%' for curried functions. xs.reduce(_+_)? Though I guess for perf reasons it might make sense to keep things as tuples so it would be xs.reduce(%[0]+%[1]).
I originally thought you were referring to this as a "hack proposal" as a way of denigrating its value, but in fact it is the "Hack proposal" where "Hack" is Facebook's fork of PHP.
I really enjoy writing me some of that F#, esp. with Bolero, but I would take `value |> foo(%)` over actual F#'s `value |> (fun x -> foo x)` any day. However, I agree that the "F# proposal" comes across as better, esp. when we consider -copypasting- consistency of the language. Now, considering everything in JS already, you are right, we did it again, didn't we? After couple more years, the Perl and Javascript languages will finally merge and become one.
On the one hand, I tend to agree and I dislike making JS syntax even more complex than it already is.
On the other hand, it works well in F# (and the ML that then borrowed it from F#) because it’s just a standard infix operator there and everything is already curried which is definitely not the case with JS. It means you will nearly always have to use a lambda when piping in JS which is honestly a bit tedious.
The proposal is not finished yet (and might never been). Pessimism aside, there is a whole issue on why it was decided on following up with Hack's implementation [1].
You can always give your input on such decisions, the % token is still being "bikeshedded"[2] (is that a word?), and there's still a possibility of making follow-ups proposals that could implement some F#-esque implementation
Wouldn't it be better to use '->' as the operator? This is about dataflow so an arrow would represent that nicely. Whereas '|>' doesn't really "mean" anything. An arrow means that something flows in the direction of the arrow.
I also pushed for this in TC39 which was shut down for seemingly no reason. Since only one language with almost no user base uses |> to mean expressions, people are guaranteed to get the wrong idea about how it works. It almost seems like intentional misleading.
It's more common for pattern matching Java's new syntax, Erlang, Elixir, StandardML (fat arrow), F#, Kotlin, Ocaml, Haskell, Rust (fat arrow), Ada (fat arrow), Scala (fat arrow), Zig (fat arrow), and probably a bunch of others too.
That's a lot of prior art for people to be comfortable with and as the fat arrow already has another meaning, overloading it in pattern matching might complicate things.
Right but then it would not be the same as in those other languages which use fat arrow anyway.
Another syntax for pattern-matching could be simply ':' instead of the arrow.
In the early days of Smalltalk the Smalltalk return statement was simply '^'. That could also be suitable for pattern matching. The idea would be that the switch statement returns something meaning pushing it up from the expression to whoever called it. So '^' might be good for that. Whereas pushing the results to the right to the next expression could be ->.
The F# syntax looks more consistent with idiomatic JS.
It always seems there’s some obscure edge case that derails the nice path for these spec proposals though. I haven’t tracked the conversation on this one, but wonder why they didn’t go with it.
Someone at google didn't like it, and apparently that overrode the entire original intent of the proposal. I'm all for industry participation but I was pulling my hair out with this.
Hardly a big win and not at all a win when you realize that you can't copy/past code to and from that syntax without risking weird errors due to the special symbol.
`await` isn't a function and (as I noted) either wouldn't be possible or would require special syntactic consideration for F# syntax, but the hack syntax is basically one giant ball of special syntactic considerations.
I'd rather explicit async/await and keep the simplicity of the F# syntax.
I don't see how syntactic sugar makes any sense for javascript. Breaking compatibility is so severe because every browser needs to catch up, yet it doesn't actually enable anything that couldn't be done before.
It lets a transpiler like Babel or Typescript use the new syntax and output the equivalent code that works on older browser.
Same reason async/await was useful far before there was native browser support - you could immediately use it in your codebase and compile to a legacy browser target.
If you're compiling from another language you would be using whatever syntactic sugar you want, why break compatibility just to have the compiled javascript look a little better?
TypeScript isn't another language though. It is the latest official ECMAScript plus type annotations. Only some very, very few, rare, old stuff like enums really is different code. 99% of TypeScript is just "remove the types to get ECMAScript".
That TypeScript, the tool,also adds a transpiler is a distraction that made a lot of people believe TS is a different language. But the TS folks have always taken great pain to only ever support features that are or are about to be in the ECMAScript standard, and not to deviate from it. That they did initially with some namespace stuff and enums was before ES2015, when JS was lacking some things many people thought were essential. Even then they only added less than a handful of "TypeScript-code".
When you look at the Babel "transpiler" for Typescript, before they added a bit more for class stuff, it pretty much showed that "transpilation" of TS to JS - as long as you targeted a recent ES version - was achieved by doing nothing more than to remove all those type annotations.
I'm still mad at the TS guiys for muddying the waters so much by confusing soooo many people by bundling type checking and transpilation in one tool. This could have been much more clear. I too stuck to using Flow for quite some time until I realized TypeScript really is Javascript, while Flow communicated in its architecture and usage already that it just "added types" (literally).
You're completely missing the point here. If you are transpiling something, you don't need to worry about syntactic sugar in the base language you are transpiling to, only what you're transpiling from.
No, you don’t get it. Babel is for transpiling newer versions of JS to older ones, typically based on targeted browsers or Node runtimes.
JS is a fantastic fp language and pipe/compose is commonplace for people writing in that style already. This just adds first class support to the language
> No, you don’t get it. Babel is for transpiling newer versions of JS to older ones
For all intents and purposes, JavaScript with all the extra features it has accreted over the years is a different language from the JavaScript of a decade ago, and a compilation step is necessary in order for web browsers to parse it.
If you're running a compilation step anyway, you may as well write the code in a language that isn't such a dumpster fire.
> This just adds first class support to the language
If it was a different language you could say it "just" adds something, but javascript is not compiled and any syntax change means you start over as far as compatibility goes. It is unique in its scope and usage and this feature doesn't enable anything new for users and is only marginally useful for programmers.
Browsers add support for new JS features every year. You can use them or not, I don’t care what you do. Every web stack I’ve worked on in the last 7 years has a compile step, too, so you’re either ignorant or being intentionally obtuse. Either way it’s not like anyone should listen to you.
So what’s your point, other than communicating how upset you are over a programming language? That we should be writing websites like it’s 1999?
I'm not sure why you're having a meltdown over this. You still haven't answered why someone needs compatibility breaking syntax sugar if you're already compiling to javascript.
>TypeScript isn't another language though. It is the latest official ECMAScript plus type annotations. Only some very, very few, rare, old stuff like enums really is different code. 99% of TypeScript is just "remove the types to get ECMAScript".
That's another language. Javascript doesn't have type annotations - even the suggested addition of type annotation syntax to JS[0] doesn't actually do anything because it can't and still be Javascript. Javascript doesn't have enums. Javascript doesn't have interfaces. That 1% (although it's probably more than that) matters. If it can't run, unaltered, in a Javascript interpreter it isn't Javascript.
It's not a different language as in "it's a different language". It's just types added. The actual executable code is pure Javascript. The type annotations have ZERO influence on what is executed, they are completely and used during development.
To call this "another language" as if it was C vs. Python does not make any sense, unless your main goal is to win some Internet argument no matter what.
Yes, the language that Typescript emits is Javascript. I can write Python code that emits Javascript, but that doesn't make Python, itself, Javascript.
Languages can be structurally or idiomatically similar but still not be the same language. And there are more differences between Javascript and Typescript than just the type annotations (although that, alone, would be sufficient.) Typescript has generics, ffs.
Anything looks better than nested calls once you have started to get used to the benefits of reading left-to-right. It's one of those pains you don't realize you have unless it suddenly goes away.
Lots of low-value names required. Those names will inevitably either be badly chosen or have taken far more time to make up than they are worth. That's assuming the code isn't written by that one guy on every team who stubbornly insists on
var x = three();
x = two(x);
one(x):
(having the names certainly is nice to have in the debugger, but I'd rather have those intermediate results be an explicit debugger feature than junk taking up mental bandwidth all over the code, at all times)
The beauty of not using variables is that they don't pollute scope: a promise to the reader that it's perfectly fine to forget about that inermeditate result immediately after seeing it passed into the next step of the pipeline. Otherwise, you never know if it will perhaps show up later for some unexpected purpose. In a left-to-right language, those values that do get a name implicitly stand out
I don't see how moving the actual thing that's returning (and it returns "toward" the left) all the way to the right end of the expression—as far away from the assignment, if any, as possible—improves readability.
Browsers have established they can roll out changes behind feature flags, and the community adapts and adopts them. This is a solved problem. ES6 transition, await/async, string templates, etc. were major JS changes that required browsers and runtimes to change. They did. It was fine. They can do it again!
Promise added a very valuable new capability, but this would just mean what you write doesn't work on any non updated browser for a very minor syntax improvement.
If there are too many nested calls, assign some of the function results to variables first.
Async/await is pure syntactic sugar. Look at python 3.0 era coroutines that were iterator/generator based as an example of async coding without special keywords. JS did not need those keywords to enable coroutines and async coding. People were already making their own promises, coroutines, etc. long before the new keywords were added.
No compatibility is broken, old scripts will keep on working just fine. Scripts written for the new syntax won't work in old browsers, but that's the nature of evolving standards anyway.
and kudos also belong to the people whove been heavily championing and bikeshedding the proposal on behalf of the rest of us for the last 8 years - which i just discovered is really nicely documented in the repo: https://github.com/tc39/proposal-pipeline-operator/blob/main...
there's usually a ton of nuance behind the syntax considerations and i usually find that the people on tc39 care way more than i do about the things i never think about until its too late. peeking into their discussions is often very enlightening... and a reminder of how hard it is to do language design by committee and at scale.
PS: The commands in Unix pipes are executed in parallel, and “|” could be interpreted as a mnemonic of that fact. However, the same is not true for the programming-language feature discussed here, which only processes singular values, not streams.
Front-end devs are generally stuck with JS, but they wish they were able to use other languages, but they can't, and can't convince their managers to use Clojurescript or Purescript, so this is what happens.
What's bonkers to me is that ECMA would prefer to keep adding features like this instead of adding a macro system.
I'm not buying that theory. I'd love if more front-end developers were eager to use PureScript or Elm, but popular sentiment appears to be that anything other than JavaScript is weird or hard or impractical or unreadable.
Javascript is, without exaggeration (and with much chagrin), the language that has done more to popularize functional programming than any other programming language in history. It's only natural that they'd continue pushing on that front. :P
But clearly we need "that kind of programming language" for rich client web apps. We need many of the modern language constructs in such a language. Would you advocate for an entirely new language to meet that need?
This is perhaps a controversial opinion, but I don't think that you need this at all to do programming. This is just syntactic sugar! Type some parentheses and move on.
There is an increasingly large population of "programmers" that have never known anything other than Javascript and don't understand the concept of "different languages for different purposes". They want Javascript to do everything so they don't have to leave their comfort zone.
I can't say I blame us, as its the utility lingua franca for UIs, especially with cross platform UIs via React Native or NativeScript.
Web browsers are a long ways off from WASM being acceptable as an alternative in most cases, as it yet can't access the DOM directly.
Its not that we don't understand different languages for different purposes, its more what real alternative do we have here and if we want certain features in the language we have to follow the process to get them.
"There is an increasingly large population of "programmers" that have never known anything other than Python and don't understand the concept of "different languages for different purposes". They want Python to do everything so they don't have to leave their comfort zone."
1. Javascript is at its core a functional programming language. In many ways it is more like lisp than like java (and lisp was the original intended syntax for javascript!)
2. Adding pipe operators is 100% in line for a functional programming language.
3. Even if it weren't, adding functional features to a high level language is definitely a good thing.
That's the real-world example they have (I reformatted the second one slightly, because it looks better to me). Neither seems very good to me, and the |> version doesn't really seem "less bad".
Which seems clearer than either because it splits out "print env variables" and "print out node args". And it would be even better with some sort of helper to convert an object to k=v string:
> In the State of JS 2020 survey, the fourth top answer to “What do you feel is currently missing from JavaScript?” was a pipe operator.
Is the wrong way to go about language design. Everyone wants something different, and if you just implement the "top 5 most requested features" you're going to end up with some frankenbeast of a language.
Having been watching, and a couple times playing with Rust... would definitely like a similar pattern matching system in JS. I still feel the C# syntax for this feels a bit alien `varname switch {...}`.
Is there another JavaScript feature that looks like a list of function names but which causes actions to be performed? Or operators that operate on both functions and values? The placeholders are very similar to variables, so do what they look like they do, and it's only the implicit definition that is new. The explicit placeholders feel much more like JavaScript to me than the implicit function calling of the F# proposal.
It absolutely is, but time and time again TC39 has followed few champions over the waves of people favoring F# pipes.
It's incredible to me TC39 would rather have this monstruosity over F# pipes which are actually pretty similar to how most pipes work and read in most functional languages including unix `|` one.
On the flipside, I think that this is still such an ongoing debate is part of why the pipeline operator is still stuck in Stage 2 and having trouble getting into Stage 3, despite being listed as a "priority" multiple times on and before 2020. TC-39 isn't blindly following anyone here, they seem to be dragging their heels hoping that someone comes along with an even better compromise between the styles.
TC39 proposals often have rubbish real world examples that should never see the light of day in a JS codebase.
It makes me really question the judgement of the people that are working on this language, and it explains how some of the shittier proposals manage to slip in and why the good ones are misused all over the place in every real world codebase when this is the kind of guidance devs have on where to use fancy new features.
In a few years everyone is going to collectively lose their fucking mind once again and decide that ternaries are now the devil and every React component should be chock full of do expressions. I can see that coming clear as day. That might finally be enough to get me to throw in the towel if the also incoming decorator hell doesn't do it.
I like decorators as a concept... but there have been a couple different implementations now... the TypeScript version is probably the most broadly used, but there were others before it via Babel (formerly 6to5).
I was an early adopter of the original decorators proposal as well as the F#-style pipeline operators. The more time has moved on and other bits made it into JS proper, I'm far more inclined to stick to what's "in the box"... Have even considered just writing straight JS + ESM (modules). Of course, I also like JSX, though not sure of any proposals of how to get that "in the box" as e4x died on the vine and a lot of other efforts didn't gain much traction either.
> TC39 proposals often have rubbish real world examples that should never see the light of day in a JS codebase.
This example was literally taken directly from real-world code in the React codebase (a script to call jest-cli). While you're correct that there are probably a lot of clearer and more readable ways to write this snippet, the fact of the matter remains that people write code exactly like these "real world examples" all of the time.
Yes and those people with a proven track record of writing awful code will now have another new tool to apply their valuable skills with. I can't wait.
React component should be chock full of do expressions
If the motivation behind these features are React components, javascript clearly lacks proper logic in array (and object) literals, like
'[' if (cond) […]expr1 [else […]expr2] ']'
Although it seems pretty strange to add syntax only because some specific user interface library together with a specific language extension could benefit from it.
Just because pipe's aren't the ideal solution for that convoluted example they decided to list doesn't mean they aren't still extremely useful for a consistent set of problems. Like anything they can be abused to make code less readable.
I also have a feeling this doc lists every sort of usecase, not because they are advertising it as "always the better version", but because it's a design doc that needs to factor in edge cases, such as using a pipe following chained function calls.
I'm just using the example they posted in the README as an "before-after". I think that's a reasonable thing to do when evaluating "do I think this would be a good feature?" Blame the author(s) of that document if you don't think it's a good example.
I don't see design docs as a tutorial on how to be a better programmer using a new syntax, the goal is to flesh out a new concept built on some fundamental ideas.
You cherry picked one example of a tangled/messy block of code that they used to communicate specific idea around "left to right" comprehension and flow of the data using the new syntax. For that specifically it did a fine job.
But that doesn't mean that's the way you should be writing code in the first place, given it started with a mess and only used one piece of syntax to change it.
I will admit it's a poor example to open with. But for a design doc about exploring and debating ideas it's fine.
The way I read the proposal is that they used a real-world example from a commonly used codebase (React) to demonstrate how this feature would improve it. I think that's a good approach as features are intended to address real-world concerns, and concrete real-world examples help with that.
I don't think it's fair to say it's "cherry picking" to focus on the example they focus on themselves. They have a few other examples too, but I would say "I don't see how [NEW] is better than [OLD]" for many of those as well (and for some, I think the [NEW] is significantly worse).
Considering it was their opening example (which they reiterated multiple times) I will concede it was a poor choice. Since yes one of the top goals is selling the general idea. So it's fair the general audience would take it at face value, especially when they use it multiple times as a real world use case, it is hard to take it any other way.
Plus the doc is #1 on HN after all, which could IRL push it beyond a proposal stage if done right.
For me not. In the Pipe example you must read first that something is done with the object and that it will be output at the end.
If i read a code and i want to now what it will do, i wanna first read that it will output something. If that is what i search for, i read the nested code.
With the pipe style i waste more time, even if the code looks clearer.
Method chaining is better, but you need the library/class/ code to support it. If you got a bunch of function it can’t help you. Pipe operator is useful for when you use other people’s code.
On the other hand, pipe is less useful when you don’t have partial application of function built-in. So yeah I think this is not worth it.
This works when your language doesn’t have an implicit “return nothing” as it’s default. Nothing says “imperative” like “this routine may or may not take input, but you ain’t getting nothing back out of it.”
Ironically, OO gets thrown under the bus a lot lately because it ain’t functional. But in reality, early OO languages like Smalltalk and CLOS were much more functional in this regard, you always had an implicit return of self, which could be chained easily.
I (over)use the piping operator (|>) in Elixir a lot. I just like the way it reads. But one thing I don’t love, is that it’s not an easy sequence to type.
Elixir's pipe mechanics probably aren't a great model for implementing pipe operators in existing languages. A large part of what makes |> work in Elixir is the steadfast commitment to f(most_likely_to_be_piped_param,other,params) argument ordering in the standard library.
I do prefer the piped version mainly because it removes annoying nesting but that's not high on my prioritized list of discomforts. The issue of deeply nested function calls is a thing, but I just use some temp variables to avoid it when it really starts looking ugly.
Then again, this is my typical opinion to a lot of the proposals - I see what this is useful for but I haven't experienced enough pain to really argue for it. However, if it does make it into the spec then I will use it probably because it's there.
I also recognize my stance as one that not many people like in other contexts because it can be interpreted as me not looking to improve my situation. But on the flip side I think cluttering the language specification with a lot of superficial syntactic sugar is a mistake.
> Everyone wants something different, and if you just implement the "top 5 most requested features" you're going to end up with some frankenbeast of a language.
Very much this. They need a new question on the survey "does JavaScript need new syntax or can we just leave it alone and work on perf/tooling/etc. of the existing stuff without throwing a bunch of new junk in"?
So their argument in favor of this fugly new operator is that some people write unreadable code? That's not the language's fault. As they say, you can write FORTRAN in any language. This just gives them a new tool to make things even worse.
I won't speak about the specifics of the chosen syntax (Hack/F#) but in general - absolutely.
With pipes you can visually follow the manipulations and function calls in the order that they happen instead of being forced to scan the code inside-out & outside-in, matching parentheses and function call parameters in your head, while still visualizing intermediate results to get 1 final return value.
I find Elixir code much easier and quicker to understand, in large part thanks to its (admittedly, imperfect) pipe syntax. Code written in this way is also much easier to debug because you can quickly add `console.log`, breakpoints, or equivalent between the pipes.
I find this unnecessarily time-consuming and difficult to parse and I'd likely raise some flags in a code review:
personally i detest pointfree syntax. having intermediate values makes it much easier to step through code with a debugger & see what is happening. and it gives the reader some name for what the thing is, which is incredibly useful context. the enablement of pointsfree styles is one of my main concerns about potential pipe operator syntaxes: the various syntaxes that have been raised often introduce implicit variables which are passed, and i greatly fear the loss of clarity pointsfree style brings.
maybe there's something beyond the pointsfree vs not debate here that i'm missing, that makes you dislike the refactored example. personally i greatly enjoy the flatness, the step by step production of intermediate values, each of which can be clearly seen, and then assembled in a last final clear step. that is much more legible to me than one complex expression.
(1) named intermediate values are sometimes more readable ... though I have examples where it's very hard to come up with names and not sure it helped
(2) debugging is easier.
For (2) though, this IMO is a problem with the debugger. The debugger should allow stepping by statement/expression instead of only by line (or whatever it's currently doing). If the debugger stopped at each pipe and showed in values (2) would mostly be solved. I used a debugger that worked by statements instead of lines once 34 years ago. Sadly I haven't seen once since. It should be optional though as it's a tradeoff. Stepping through some code can get really tedious if there are lots of steps.
Intermediate variables also have the benefit to make not just the last value available in a debugger view, but also previous values (stored in separate variables). Of course, a debugger could remember the last few values you stepped through, but without being bound to named variables, presentation would be difficult.
It's hard to understand which statement the debugger has a break point set to when you can put many breakpoints on the same line
I have tools that can do it, but I'll still have a better time splitting out a variable for it, especially since what I really want is a log of all the intermediate values, so I can replicate what it's doing on paper
In languages that more easily support repl-driven development (e.g. Clojure), I think this is less of an issue. If you have a handful of pure functions, you can quickly and easily execute them via the repl, so you get a lot of clarity as to what those intermediate values look like even if the functions are ultimately used in a more point-free style.
But on the other hand, this would be a nightmare in C# (what I use in my day job). Sure, you can execute arbitrary expressions while debugging C#, but IMO you can't really achieve the same clarity. I'd rather see intermediate values like you suggest since it's easier while debugging, vs a bunch of nested function calls.
Do you mind elaborating? I find the refactored version significantly easier to understand than the original. Readability is one of the top priorities for me and I find the original example too clever, in a bad way.
I dislike variables that are used just once. Sometimes they're a "necessary evil", but rarely. For "envStr" it's defensible IMHO as it splits up some of the complexity, but I would rather just use a helper function, which has the same "splits complexity" effect and is re-usable.
Extracting `envStr` is definitely the highest impact change for me. I dislike temporary variables too when they don't represent a meaningful intermediary result, but in this case I see them as a lesser evil. I agree that `styled` is more about personal preference.
This is why I'm happy to see the pipe syntax proposal, it avoids unnecessary temporary variables while simultaneously aiding readability.
> This is why I'm happy to see the pipe syntax proposal, it avoids unnecessary temporary variables while simultaneously aiding readability.
The thing is I don't think it's all that much more readable. No matter which syntax you use, there's still the same number of "things" going on in a single statement.
I do have to admit I never worked with a language that uses |>, so I'm sure that with increased familiarly with this it would become "more readable" to me, but one has to wonder: just how many calling syntaxes does one language have to support? More syntax also means more potential for confusion, more ways to abuse the language/feature, more "individual programming styles", more argueing over "should we write it like this or that?", more overhead in deciding how to write something, harder to implement the language and write tooling for it, and things like that.
There is always a trade-off involved. The question to ask isn't "would this be helpful in some scenarios?" because the answer to that is always "yes" for practically any language feature. The question to ask "is this useful enough to warrant the downsides of extra syntax?" I'm not so sure that it is, as it doesn't really allow me to do anything new that I couldn't do before as far as I can see. It just allows me to make things that are already too complex a bit more readable (arguably).
Truly, one of the main reasons I might stop increasing the complexity of a bash one-liner and move it to a full shell script, or bail for Python, is specifically so I can turn those chains of pipes into imperative steps with temp vars so that they're actually legible and easy to reason about. I can't imagine why I'd want to go the other direction.
Those variables help to document intent by naming the intermediate values. They also make step-debugging more convenient. In languages with type declarations, they also serve to inform about the type of the intermediate value, which otherwise is invisible in a pipe sequence.
IDEs for languages with pipes allow break points on pipelines and can show inferred type annotations mid pipeline. Given the popularity of JS, this tooling will appear rapidly after pipe standardisation.
In the beginning I wasn't a fan of it, as I do a lot of Haskell (and have point-free idioms on the tip of my fingers), and it's obviously unnecessary, as you said yourself. But with time I learned to appreciate this kind of function for its simplicity and consistence.
Now, of course, with a pipe operator (or other similar constructions) you can get the consistency without the intermediate names.
I just find it easier "thing" that happens is one self-contained statement, if that makes sense. Makes it easier to see what does what. I don't think one way is "better" or "worse" btw; all I can say to my brain, I find it harder to follow. This is what can make programming in a team hard.
I'm also one of those people that likes single-letter variables. I know some people hate it with a passion, but I find it very convenient. Just makes it easier to read as there's less to read.
I'm not smart enough to do Haskell, so I can't say much about that.
Oh, a kindred spirit. I also love single-letter variables where they make sense. I have a math-heavy background, so they're totally cool for me, BUT I get why most programmers would hate 'em. I also like them since they were the "norm" in old C# LINQ code where I learned functional programming. If I were alone in the programming world I would have written the example above with single letter vars.
I agree with your remarks, there's no right or wrong, it's like tabs and spaces.
Btw I'm also not smart enough for Haskell, but it hasn't stopped me so far ;)
The refactored version is doing what pipes would do in a language that doesn't support pipes; it's reordering the statements into execution order rather than having to use a mental "stack" to grok the original version.
Given that the OP stated that they like the pipe syntax, and the refactored version illustrates the hoops you'd have to jump through without it, I guess your comment is just a strange way to agree with the OP?
Late reply, but I find that with a bit more of modernizing, and more consistent usage of chalk's arg-concatenation feature this can be turned into something much terser:
> Neither seems very good to me, and the |> version doesn't really seem "less bad".
For me the piped version seems way better, way cleaner, with cleanly separated processing steps.
I don't like nested steps because for nesting multiple arguments functions your expression tends to grow in both directions and parts belonging to the same step tend to end up really far from one another and clearly distingushes them from data that's pushed through the pipeline.
% is short so it keeps parts of the same processing step together.
This syntax also enables you to express which part is the data to be processed, what are the processing steps and what are additional parameters of the processing steps.
The F# syntax looks/acts a lot better here (especially paired with lodash). I also feel your code example wasn't done in the way people would actually use pipes.
Sure, I know. What I actually mean is that I don't grok curried functions.
Does join(a)(b) mean join(a, b) or join(b, a)? Does it mean a.join(b) or b.join(a)? And is a or b the delimiter? I don't really feel confident without looking it up or trying it out. I have a similar problem with Haskell's function syntax a -> a -> a.
If the function was called makeJoinerBy(sep)(array), it would be somewhat clearer:
Having done some personal projects in F#, I'm a huge fan of the syntax. However we would need most standard functions in JS to be curried to take advantage of it.
That's why I prefer the F# version where it's just the names of functions or anonymous arrow functions. Nothing magical about them. It just calls the functions in order from top to bottom.
All of the examples look unreadable and hard to debug to me. Why not something like this rather than one massive nested instruction?
let keys = Object.keys(envars);
let text = keys.map(envar => `${envar}=${envars[envar]}`).join(" ");
console.log(chalk.dim(`$ ${text}`), "node", args.join(' '));
That way you can easily inspect and verify the intermediate values at runtime. Helpful for you to see if your code works as expected, helpful for others to see what the code is doing.
Bad examples can doom a project. It's amazing how much time people can spend on a solution while completely ignoring the documentation.
Pipes are probably better used on a set of operations that takes input and runs multiple functions on the input, rather than fiddling with string concatenation multiple times.
What do we use multi-pipes for in unix? I can't recall the last time I used one where the middle action wasn't a filter of some sort. grep -v to remove lines, or colrm or awk print to cherry-pick fields from a multi-field line. Once in a very long while I need to do three commands with filters between them. Beyond that it's too complicated and I make a script to handle several of the steps.
The issue with taking examples from real-world code & converting them is that there's no guarantee the real-world code is good. It usually isn't, so you're comparing bad with bad.
A more aggressive reformulation would be to prefix the original code with
This more clearly highlights the obvious limitations of pipelines - piping envOutput but not 'node' nor argsOutput is a jarring syntax-mix here. Though I think it offers some hope for them working well in other scenarios - possibly in curry-heavy applications.
It's not jarring. It expresses what is the subject that's being processed and what are additional parameters of the processing steps. This allows to keep all parameters of each processing step together and processing steps easily visually separable.
> It's not jarring. It expresses what is the subject that's being processed and what are additional parameters of the processing steps.
Only if the first parameter of the function is the sole subject & subsequent parameters are "additional". Which isn't the case in this example: all params are equal subjects.
Right. `chalk.dim()` is probably not the best thing to use as an example for this.
But you can still think about this as merging in two additional pipes into the one you are processing. So 'node' and argsOutput are just very short pipes that you are merging into the flow of current one.
Is that how they really do it? I haven't actually seen that happen. There's no reason not to take popularity into account for some measure, as long it's not the ONLY thing you take into account.
> I also feel this:
>> In the State of JS 2020 survey, the fourth top answer to “What do you feel is currently missing from JavaScript?” was a pipe operator.
> Is the wrong way to go about language design.
Let's call it Signor Rossi language design…
"Signor Rossi cosa vuoi? … E poi, e poi, e poi" (Viva la felicità by Franco Godi [1])
During the 2020 survey there were two versions of this operator on the table smart mix and F#. A few months later the committee advanced a third option Hack, which is kind of like smart-mix but always requires the placeholder token.
I suspect that many developers expressed their desire for this operator under the believe that it would make their style of programming easier, which F# indeed does for many code bases, particularly those that use libraries such as fp-ts or rxjs.
However the advanced version fails to deliver that
So it's basically Hack syntax for pipes with just ;_= instead of |> and _ instead of %. And you need to 'mark' the beginning of the pipeline with `let _=`
Additional 'benefit' is that until you leave the scope you can access output of the last pipe through _.
You can always use ;_= for consistency and pre-experess you intent to use the piping in the current scope by doing `let _;` ahead of time:
The thing I dislike about it most is the constantly-rebound % variable. It means something different in each line. In this case they have elected to keep it as a string throughout the pipe, but this ‘more pipey’ version of the code has it start out as an array then turn into a string halfway through the pipe, which feels dangerous (and is presumably why they didn’t take the example this far):
To be honest, I've pretty much given up the hopes that TC39 would actually resolve pipelines and decorators at this point... I think it's been around a decade now.
But then why would you call all those different variables ‘%’?
Reading this version though I also notice something else that the F# style enables that the Hack style doesn’t which is that it supports destructuring. So in F# style, I can more easily make pipe steps that pass on multiple values in a structured object or array, then access them easily down-pipe.
Unfortunately F# pipelines have been rejected a few times by TC39 to advance... I used it for a while via Babel/6to5, but I gave up (like with decorators) after many years of no advancement. I doubt I'll see either any time soon.
The flaw with that approach to language development is that there's no limiting factor.
Can you imagine a survey where everyone says they're satisfied with the language as-is? Of course not. That's statistically impossible. Even if 99% of the needs of developers are satisfied by a language, people are going to eventually answer that they'd like something that the language doesn't have, and that's always the case.
Let's say JavaScript implemented nearly every single language feature that's ever been invented. That is except the goto statement. Upon being surveyed on what they think is currently missing from JavaScript, a significant number of developers will have to respond with goto. Does that mean JavaScript is actually "missing" goto and that it was a mistake that it was never implemented in the first place? Of course not.
As somone that use to write a lot of functional-style code (in ruby), and generally prefers functional style, and have created many many many "pipelines" like that in ruby code - I've actually started to "regress" to "status-quo" (as the article puts it) mostly because I work with developers that don't understand functional style and it just becomes point of contention in review that I just don't care about getting into anymore. I can see this same kind of thing happening with this operator in JS-land (I may be wrong, haven't written any significant javascript in years, but people tend to be stubborn).
I just write things as stupidly as possible now, and just do the second one even though there are nicer "ruby-ways" to do them - maybe this is "bad" but I find it easy to read and grok...and it doesn't cause arguments during review <.<
> Is this… ${new-style} really better than… ${old-style}?
Yes! A hundred thousand times yes!
I have not seen this syntax in JavaScript before, but just by having the vague notion that it's pipe-like, I was able to generally understand what was happening within seconds of reading it. I'd have to read up on the syntax a bit to confidently write code in this style, or perhaps to fix a bug (does "${%}" do what I think it does) but I can quite literally comprehend the author's intent almost at almost the same speed as I can read.
1. get all the keys of envars.
2. convert every key into a "var=value" format.
3. put a space between them all
4. stick a dollar sign in front of the whole thing
5. no clue what the `chalk` library is for?
a. this *appears* to prefix a "node ${args}" call with the previously-created environment variable bits
b. that tracks with what we've seen so far
c. close enough for now
6. log the whole thing
Note that even without knowing what chalk is or does, the flow of everything makes it extremely clear what exactly the high-level outcome is supposed to be. We're building up a string like "$ FOO=bar BAZ=qux node --arg thing --arg2 more_args". We're doing something fancy with it that I don't quite know about just yet, but the above knowledge makes it very easy to fill in that gap.
With the second one, I have to construct a tree in my head
1. console.log…
a. the result of chalk.dim, which is
i. all the env vars
ii. mapped to key=value format
iii. joined by a space
b. ^ actually do chalk.dim on that thing above
i. sorry, back up, there were some additional arguments
c. ^ actually do chalk.dim on the above with 'node' and space-separated args
i. I think this concatenates them?
ii. double-check [1.a] to confirm
iii. did I miss `args` defined somewhere in [1.a]?
A. no, it's apparently inherited from scope
2. ^ actually log all the above
a. wait is this a correct reading, based on reassembling the above?
The confusion is compounded by not knowing the details of chalk. I have to jump backwards and reason about what the result of a non-linear chunk of steps is to make sure my guess is consistent.
I have no opinion on the merits of this particular syntax, its implementation details, or in comparison to alternate proposals of similar ideas. I'm sure there's worthwhile debate to be had on those details. But from a high level, I'm a fan. My head is not a meat-based tree traverser and I'm guessing most people's isn't either. Human brains are big fans of linear narratives. While I love movies and shows like Memento and Westworld, this kind of disjoint storytelling is quite literally done for the purpose of generating confusion and not for promoting comprehension.
Also, there may already exist better ways of expressing this particular example linearly and succinctly. If there is, I'd probably prefer that. Worst-case scenario you can always deconstruct nested function calls into sequential variable assignments, and maybe that's the "best" answer here instead of new syntax. But is an approach that can be understood linearly from start to finish clearer than one that requires mental tree-walking? Yes. Yes yes yes yes yes.
JavaScript has been a frankenbeast of a language right from its inception, by design.
However, the 5th most requested feature in that poll is somehow "functions", and "sanity" also appears in the results, so this particular source may not be a good one.
It's annoying to have to decide on and write out so many names. The intermediary names are not relevant to solving the problem. This is so much less noisy:
The intermediary names are extremely relevant to the next poor sucker who has to understand what you were trying to do.
Code is read far more than it is written. Use temporary variables. Put in the effort to name them once, and then that effort pays back every time anyone needs to read and understand the code.
> The intermediary names are extremely relevant to the next poor sucker who has to understand what you were trying to do.
There are lots of cases where they’re not and are just noise written in exactly the style of the original comment. Or worse all the one-shot temporaries get assigned to the same worthless name.
Unless you live and die by the mantra that no expression can have more than one period, pipes pretty much just bring attribute/method chaining to arbitrary functions and expressions.
More generally though, I don't see why forcing everyone to write out intermediary names all of the time leads to more readable code. If it's more readable to do so, I will. If a pipeline is more readable, why should we be prevented from using it?
> you can add helpful comments to pipeline code if needed
The pipeline with explanatory variables explain to you what the steps are with code. Using pipeline you need to add comments to explain "what" you are doing.
I think this is way better. The variable names tell me at an instantaneous glance what each clause is doing. I don't have to spend mental effort delving into what's going on with the lambdas, or scan back and forth to find a comment that may or may not be there or out of date if it is.
Furthermore, I'd wrap all of those lines together into a getHighestScore() function as well. That makes the complexity exactly as visible or as abstracted as you want at any given moment. The name of that function tells you what the aggregate of the operations is doing, and you can go look inside that function if you want to see the individual steps.
It's only less noisy because of the simplistic nature of the example. In real world code, `one`, `two` and `three` and probably a chunk of code put together in a single line and it's difficult to find out what the hell they are doing. Concat together a few of these and that's a recipe for disaster. Less experienced engineers would add a comment at the top of the pipe chain explaining what's going on. More experienced engineers would divide and conquer and use temporary named variables (render commets useless).
While pipes are great in Elixir I think it’s important to look at the total cost to adding any new syntax in the context of the language that is considering them.
In the JS ecosystem, idiomatic nested function calls are read from the inside out. And in most cases that is highly readable because a single layer of nesting is all you need.
This proposal flips that on its head, so you have to retrain yourself to read code in a new way based on the presence of a |>. That adds significant cognitive overhead, especially in code that isn’t well written or that is already complex.
Your example looks nice, but also just as nice is the .then() syntax you could have used.
Now we’ll see code bases with both styles. You’ll have some team members who love pipes so much they’ll never call a function the old way doing x |> console.log and the rest of the team doing console.log(x).
This is not elixir pipes. Elixir pipes are sound because they are piping to a function. Piping to a statement is not the same thing and it will become more clear in actual usage just how bad these are. Remember % in js is also modulo, and "test" % "ing" returns NaN. NaN is infectious since basically anything that operates on NaN returns NaN. "test" % "ing" + "ana" returns NaNana . You can quickly see how these pipes (as implemented) are so bad they might as well be language sabotage.
That is concise, readable, and does not have any room at all for error handling. If this was Rust, it could at least be turned into something which returned the right Err for what was happening.
Without something like that, trying to add error handling to the things which may blow up would instantly turn it into gibberish. Every single function here can fail (the HTTP request could fail, the data returned could be unparseable as JSON, or not have the right format). We should be trying to make our languages encourage us to write code which can handle those errors naturally, rather than encouraging us to write fragile code.
But it allows a fully functional style, and so you can do railway oriented programming[0], where every function has a success and failure route. The top level code looks the same as that without error handling.
Personally, I don't use classes much, but sometimes I think free functions are a little too hard to find, so I tend to experiment with the following pattern.
interface User { … }
const User = {
rename(user: User, newName: string): User { … },
getDisplayName(user: User): string { … }
}
const renke: User = { … }
console.log(User.getDisplayName(renke));
Which makes finding an operation for a certain type easier to find (just write User and trigger autocomplete).
The alternative is of course having renameUser (or userRename) and getUserDisplayName (or userGetDisplayname). The prefixed version would make autocomplete easier also.
This is the beauty of pipeline operator. It adds very little extra machinery - it’s just an infix operator that makes working with free functions easier.
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.
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.
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.
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."
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:
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.
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?
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.
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
);
}
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.
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.
> 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?
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.
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.
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.
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?
> 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.
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.
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'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.
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.
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.
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.
Excruciatingly contrived, but does this sort of arithmetic work in the Hack syntax? I'm genuinely curious, couldn't find any mention of modulo (or remainder) in the proposal.
> ...so only the first usage of `%` counts as a replacement?
Hopefully not because there’s no reason to: in the same way you can use + or - as prefix or infix, % as value and % as binary operator are not ambiguous.
% % %
should not be an issue, though it’s useless and not exactly sexy looking.
> I'm surprised they aren't going with an idiom like `$1`, `$2`, etc
That makes no sense, $1, $2, and $3 are different parameters.
Using your example,
|> `${$3.id}: ${$1.friendlyName} ${$2.url}`
makes absolutely no sense.
Not to mention the very minor issue that $1 is already a valid JS identifier.
> or something like in other languages that have "magic" lambda parameters.
% is one of those, it’s what closure uses for its lambda shorthand. Scalia uses `_` and kotlin uses `it`.
The latter two are ambiguous but I guess since pipes are new syntax there wouldn’t be a huge issue making them contextual keywords.
Most language with “pipelines” are curried so it’s not a concern, and in the rest it tends to be a fixed-form insertion, so it’s quite inflexible, but in both cases APIs are designed so they play well with that limitation e.g. in curried language you’d have the “data” item last (that’s obviously in Haskell), which also allows for partial application in general, while in “macro” languages, well, it depends where you decide the magical argument should be inserted (IIRC in Elixir it’s the first, so functions written for pipe compatibility should take the main operation subject first).
Clojure is cool because it has both plus a macro where you give it the name of the substituted symbol. However being a lisp the pipe macros are still prefix, not infix.
>That makes no sense, $1, $2, and $3 are different parameters.
>Using your example,
|> `${$3.id}: ${$1.friendlyName} ${$2.url}`
>makes absolutely no sense.
That's not what I was saying. I was saying that using `$1`, `$2`, and `$3` _would be different parameters, which would be good at helping disambiguate_.
That would enable this, from my example:
|> `${$1.id}: ${$1.friendlyName} ${$1.url}`
while also enabling something like this:
|> `$1.indexOf($2)`
...whereas just sticking with the single `%` means you _can't_ disambiguate, and that if they instead try to allow disambiguation by deciding that the first `%` is `$1`, and the second `%` is `$2` (and so on), then now you can't use the template-string example I gave.
Having unique "magic scope variables" at least allows you the flexibility to handle non-unary use-cases.
Either way, this is another case of "every lexer/parser has to be riddled with special cases" to handle "is this `%` a fancy-pipeline-identifier, or is it an operator?"
Enable for what? A pipeline threads a value through a sequence of operation, there is no second parameter.
> ...whereas just sticking with the single `%` means you _can't_ disambiguate
Which doesn't matter because there is nothing to disambiguate.
> Having unique "magic scope variables" at least allows you the flexibility to handle non-unary use-cases.
Which do not and can not exist.
And even if they did (which, again, they don't), you could do exactly what Clojure does with its lambda shorthand: %1, %2, %3, %4.
> Either way, this is another case of "every lexer/parser has to be riddled with special cases" to handle "is this `%` a fancy-pipeline-identifier, or is it an operator?"
There is no special case, having the same character be a unary and binary operator is a standard feature of pretty much every parser. Javascript certainly has multiple, as well as operators which are both pre and post-fix.
> Deep nesting is hard to read […] Temporary variables are often tedious
I’m a little torn because pipes might be pretty nice, especially for prototype code and small projects. One thing this proposal doesn’t acknowledge is that for production code, deep nesting’s often impractical and often considered an anti-pattern in the first place, so making it easier isn’t a common need or problem to have in my experience. Usually I’m going the other way, having to make it more tedious. Using temporary variables and breaking apart nesting is, far more often than not, necessary in order to do proper error checking, and just to make code readable, commentable, and refactorable, etc. I feel like what we need is not a way to make deep nesting easier to read, it’s a way to make temporary variables less tedious, perhaps while also piping from one function to the next… that would be really helpful in a deeper way than just adding another chaining syntax. Is the syntax is stuck at “|>”? I guess it’s not possible to override bitwise-or (“|”), but “|>” feels maybe a little clunky?
This strikes me as something better left to libraries. If you want to write in a functional style then Ramda, Lodash, Underscore, and plenty of others have pipe and compose functions.
pipe(one, two, three)
Easy to read. No new syntax. Extendable with arrow functions.
Yes, there are some limitations in comparison to Hack Pipes. But those are far outweighed by not messing yet again with the language’s syntax.
In absence of |> % syntax I'd nearly always would go with intermediate temporary variables instead of pipe(). "point-free" syntax feels horrible for me and wrapping everything in lambdas feels excessive.
This only applies to TypeScript, but error checking for this pattern from a library is much harder than error checking for native syntax.
In order for TypeScript to check if the return value of one function has the correct type for the next function, you need to use generics, but generics don't allow for a variable number of generic parameters, so `pipe` would just have to use unknown or have dozens of overloads for each length of pipe usage (within reason).
I know TypeScript is a different, optional language, and I see no reason why they couldn't add the feature without it being in JavaScript, but that's not generally how TypeScript operates. If JavaScript doesn't add it, TypeScript won't. There's also lots of tooling that will type check your JavaScript when available, which will benefit from a simpler model.
The problem with that is that it is impossible to infer the type of the n-ary pipe() function. Libraries “solve” this by overloading that function with n annotations, but that really isn’t a permanent solution, especially for libraries with millions of users, as there will always be a user that puts n + 1 parameters in that function.
Can someone remind me again why there's never been movement to add a second modern language to web-browsers? JavaScript was created in a weekend and then stuff tacked on for the last 28 years.
We know so much more about how to create programming languages today than we did then, and the whole "Year of the Linux Desktop" has become a WebAssembly meme now every year since its introduction six years ago, with it getting popular always next year, next version, with feature XYZ. Seemingly creating unmaintainable/debuggable mess from external languages with no true 1:1 into WebAssembly isn't as big of a hit as the originators expected.
Yet every time someone asks why there hasn't been movement here it is "Year of WebAssembly is next year!!!" WebAssembly has managed to slow actual progression towards something good. With the browser monoculture you'd think it would be easier now than ever to start a fully integrated second language with WebAssembly compilation for backwards compatibility.
WebAssembly is just what you want, a second "modern" language that works in browsers. It hasn't reached 100% of its potential just yet, as there are some things missing for that (like DOM access) but once the language is feature complete, I'm sure most languages will have some sort of "Lang to WASM" tooling that'll allow you to write React apps in Ruby or whatever, if you so wish.
Stuff like that takes time though, so if you're antsy, you have two options: get involved, or wait patiently.
There's a few options out there if you don't mind being an early adopter. Most interactions via DOM bridging are slower than actual React... but it's kind of cool.
Yew (Rust) is one that I've been following with interest, the hello world examples are interesting enough. I know people that have been liking the direction of Blazor (C#) more, but it has a larger initial payload, that I don't care for, it's also closer to SSR approach in the browser.
The problem with both, IMO, is that they don't have a good UI library/toolkit. I find mui.com (with React) pretty much the best browser ui framework I've experienced (since 1996). To me, the first language + component library that targets WASM and that level of components will likely win.
Flutter is probably the closest example I'm aware of, though afaik it's JS as the browser target, not WASM, but wouldn't be surprised if this changes assuming DOM interop for WASM becomes better performing.
Because there's not that much wrong with modern JS, with the "created in a weekend" mantra being wholly irrelevant.. Back when JS was trash there was was Coffeescript and other lesser used alternatives, and Google tried to push Dart but nobody cared. Today is pretty much only JS and its superset TS and that's not an accident. They're perfectly productive. My annoyance with JS is almost entirely to do with Node, otherwise I prefer JS to Python, for example.
I know a few people don't like it... but I've really enjoyed the Deno take on things for JS/TS runtime. While I'd prefer more native deno, the npm/node compatibility integration has made it really useable.
As for GP, you're right... there have actually been MANY attempts at other languages for in-browser. In the end, the JS runtime has been hardened and all other roads have led to WASM being the second target. I still remember a lot of people using VBScript in IE. I was an outlier in that I used JScript for classic ASP.
WASM is effectively "a second language" to browsers. Yes, you call it from Javascript, but it enables browsers to work with "any" language instead of standardizing on a second new language and doubling their workload by needing to integrate it with the rest of the browser just as tightly as they have with JS.
There have been efforts, but none have been totally successful.
In the past you had Java applets, Silverlight and Flash. Compile-to-JS languages depend on rather than replace JavaScript, but it's about as close as you can get without being a browser developer. Chrome did originally intend Dart to be supported natively in the browser but eventually gave up that effort. Perhaps now Chrome is dominant to push it through without getting buy-in from Apple and Mozilla, but I doubt they have any interest now that there's WebAssembly.
Like it or not, WebAssembly is seen as the answer for additional languages in the browser. And for most developers JavaScript functions well enough as either a development language or compilation target.
Internet Explorer supported multiple script languages.
The DOM in IE was essentially a COM API and callable from any supported scripting language. This included JScript (their JavaScript clone) but also VBScript and PerlScript.
I don't recall seeing anyone actually using PerlScript, or other scripting languages beyond VBScript in the browser for apps. I did work in a few locations where there was heavy push for VBScript for browser. I used JScript in classic ASP in much the same way.
If you want to use wasm, just go and use it. You don't need every other site to do the same before you jump in.
Personally, I do think JS is a better fit than Rust or C for almost everything people do on the web. And the support in other languages is still too bad to use. But you can use it whenever you want, differently from a second language you may invent.
This proposal has been languishing for years and years. I'm not sure when it last made progress, but I remember waiting for it with anticipation around 2018
It's no less legible than original code and at least expresses the intent of what's being processed, what are the processing steps and what are processing parameters.
a(b(),c(),d(e(),f(g()))) is just function call soup.
There's nothing preventing you from not using the pipe for last call or even introducing that rule in your team if you can convince your colleagues it's a good idea or even automate it with the use of a linter.
If it's really good rule you can advocate for it at this stage. It might be prudent to use |> % only inside function call parameter or possibly as right hand of an assignment instead of everywhere where parser expects an expression.
a(b(),c(), g() |> f(%) |> d(e(), %))
Although I'll be honest, I don't like it. Other parameters of a() call for me occlude the flow and the intent.
We could make it a bit better with newlines.
a(b(),c(),
g() |> f(%) |> d(e(), %))
)
But if a() takes more parameters after the main one then we have the same problem as usual where parts of the same call can end up far away from one another.
435 comments
[ 6.9 ms ] story [ 318 ms ] threadIt's similar to await/async, you eventually start designing code that better suits that interface rather than pigeonholing it with complex syntax.
An aside, the implementation is kind of amusing. It almost seems unnecessary for this to be a macro but maybe the compiler can optimize this a bit more? I would expect TCO to simplify of my "simple" implementation of
https://github.com/elixir-lang/elixir/blob/a64d42f5d3cb6c327...In Ocaml, you often end up doing things like:
I'm ambivalent about adding them to JS however. It's a nice feature but I don't think it works well with the rest of the syntax.I think that the way it's done is a net-positive in designing cleaner APIs, but there are times when I've already done a pipeline, and storing the output is just the last step. This last step is just frustratingly, not always possible. I don't think one should do something like the above, it's just what you must resort to if you _did_ want to do it.
I wrote this line for a compilers class:
Actually looks like a RISC pipeline! It looks even better with code ligatures[1].[1]: https://i.imgur.com/Qwx8CDr.png
Hack proposal
F# proposal F# proposal would make `await` and `yield` into special syntax cases or not allowed.I'd rather do await/yield the old fashioned way (or slightly complicate the already complex JS syntax rules) than add the weird extra syntax. Arrow functions are elegant and already well-known and well-understood.
If so I think that should be a consideration, even if in practice there isn't massive value in a backward pipe operator in javascript. It would be a shame to later want to add it and have to add another layer of kludge and messy syntax.
The Hack-style proposal though...I don't think it would work with a backward-pipe operator at all. Not without adding a _second_ special-case symbol, at least.
The expression you see is evaluated left-to-right. (|>) is a function taking a_value and (fun a b -> a+b) as arguments and applying a_value to the function. This returns a function taking b as an argument (that’s called a partial application). (<|) is once again a function taking the resulting function and b_value as arguments and applying b_value to its first argument which finally returns the wanted results.
The issue with unary function doesn't exist in F# because every functions can be seen as a unary function returning a function taking one less argument thanks to partial application. That's the beauty of currying.
Then there are ||>, |||> and <|, <||. As well as >> and <<.
Would be nice to use computational expressions for async and generators. (https://es.discourse.group/t/add-computation-expressions-fro...).
This proposal reminds me of Scala with anonymous parameters '_'. Could I use more than one '%' for curried functions. xs.reduce(_+_)? Though I guess for perf reasons it might make sense to keep things as tuples so it would be xs.reduce(%[0]+%[1]).
On the other hand, it works well in F# (and the ML that then borrowed it from F#) because it’s just a standard infix operator there and everything is already curried which is definitely not the case with JS. It means you will nearly always have to use a lambda when piping in JS which is honestly a bit tedious.
You can always give your input on such decisions, the % token is still being "bikeshedded"[2] (is that a word?), and there's still a possibility of making follow-ups proposals that could implement some F#-esque implementation
[1]: https://github.com/tc39/proposal-pipeline-operator/issues/22...
[2]: https://github.com/tc39/proposal-pipeline-operator/issues/91
That's a lot of prior art for people to be comfortable with and as the fat arrow already has another meaning, overloading it in pattern matching might complicate things.
Another syntax for pattern-matching could be simply ':' instead of the arrow.
In the early days of Smalltalk the Smalltalk return statement was simply '^'. That could also be suitable for pattern matching. The idea would be that the switch statement returns something meaning pushing it up from the expression to whoever called it. So '^' might be good for that. Whereas pushing the results to the right to the next expression could be ->.
Just my preferences.
It always seems there’s some obscure edge case that derails the nice path for these spec proposals though. I haven’t tracked the conversation on this one, but wonder why they didn’t go with it.
If your project is going to be robust, you MUST intercept the result and check before asking for JSON.
Your hack-syntax response would then become Hardly a big win and not at all a win when you realize that you can't copy/past code to and from that syntax without risking weird errors due to the special symbol.`await` isn't a function and (as I noted) either wouldn't be possible or would require special syntactic consideration for F# syntax, but the hack syntax is basically one giant ball of special syntactic considerations.
I'd rather explicit async/await and keep the simplicity of the F# syntax.
Same reason async/await was useful far before there was native browser support - you could immediately use it in your codebase and compile to a legacy browser target.
TypeScript isn't another language though. It is the latest official ECMAScript plus type annotations. Only some very, very few, rare, old stuff like enums really is different code. 99% of TypeScript is just "remove the types to get ECMAScript".
That TypeScript, the tool,also adds a transpiler is a distraction that made a lot of people believe TS is a different language. But the TS folks have always taken great pain to only ever support features that are or are about to be in the ECMAScript standard, and not to deviate from it. That they did initially with some namespace stuff and enums was before ES2015, when JS was lacking some things many people thought were essential. Even then they only added less than a handful of "TypeScript-code".
When you look at the Babel "transpiler" for Typescript, before they added a bit more for class stuff, it pretty much showed that "transpilation" of TS to JS - as long as you targeted a recent ES version - was achieved by doing nothing more than to remove all those type annotations.
I'm still mad at the TS guiys for muddying the waters so much by confusing soooo many people by bundling type checking and transpilation in one tool. This could have been much more clear. I too stuck to using Flow for quite some time until I realized TypeScript really is Javascript, while Flow communicated in its architecture and usage already that it just "added types" (literally).
JS is a fantastic fp language and pipe/compose is commonplace for people writing in that style already. This just adds first class support to the language
For all intents and purposes, JavaScript with all the extra features it has accreted over the years is a different language from the JavaScript of a decade ago, and a compilation step is necessary in order for web browsers to parse it.
If you're running a compilation step anyway, you may as well write the code in a language that isn't such a dumpster fire.
> JS is a fantastic fp language
Actually, it's pretty terrible at that.
If it was a different language you could say it "just" adds something, but javascript is not compiled and any syntax change means you start over as far as compatibility goes. It is unique in its scope and usage and this feature doesn't enable anything new for users and is only marginally useful for programmers.
So what’s your point, other than communicating how upset you are over a programming language? That we should be writing websites like it’s 1999?
That's another language. Javascript doesn't have type annotations - even the suggested addition of type annotation syntax to JS[0] doesn't actually do anything because it can't and still be Javascript. Javascript doesn't have enums. Javascript doesn't have interfaces. That 1% (although it's probably more than that) matters. If it can't run, unaltered, in a Javascript interpreter it isn't Javascript.
[0]https://github.com/tc39/proposal-type-annotations
To call this "another language" as if it was C vs. Python does not make any sense, unless your main goal is to win some Internet argument no matter what.
Languages can be structurally or idiomatically similar but still not be the same language. And there are more differences between Javascript and Typescript than just the type annotations (although that, alone, would be sufficient.) Typescript has generics, ffs.
Firefox usage is down to a rounding error, and they happily implement whatever the commercial duo decides the web should have.
If there are too many nested calls, assign some of the function results to variables first.
And critically browsers are - for the most part - much more in sync these days across the board.
A lot of web devs got burned through the years of IE6-IE11 and have a natural distaste for browser level changes.
Pipes have been (formally) debated for 5yrs now by the JS people, so they aren't exactly being non-conservative about this one.
Their android system somehow always shipped with broken auto-update of webview or chrome.
Which results in people open your webpage with quite old browser version.
there's usually a ton of nuance behind the syntax considerations and i usually find that the people on tc39 care way more than i do about the things i never think about until its too late. peeking into their discussions is often very enlightening... and a reminder of how hard it is to do language design by committee and at scale.
In an alternative universe, Unix shell syntax may have used “—“ instead of “|” for piping.
Front-end devs are generally stuck with JS, but they wish they were able to use other languages, but they can't, and can't convince their managers to use Clojurescript or Purescript, so this is what happens.
What's bonkers to me is that ECMA would prefer to keep adding features like this instead of adding a macro system.
After all, they are all “syntactic sugar”, right?
Web browsers are a long ways off from WASM being acceptable as an alternative in most cases, as it yet can't access the DOM directly.
Its not that we don't understand different languages for different purposes, its more what real alternative do we have here and if we want certain features in the language we have to follow the process to get them.
Also, s/Javascript/$LANG_YOU_HATE/g
1. Javascript is at its core a functional programming language. In many ways it is more like lisp than like java (and lisp was the original intended syntax for javascript!) 2. Adding pipe operators is 100% in line for a functional programming language. 3. Even if it weren't, adding functional features to a high level language is definitely a good thing.
Can also write it as:
Which seems clearer than either because it splits out "print env variables" and "print out node args". And it would be even better with some sort of helper to convert an object to k=v string: ---I also feel this:
> In the State of JS 2020 survey, the fourth top answer to “What do you feel is currently missing from JavaScript?” was a pipe operator.
Is the wrong way to go about language design. Everyone wants something different, and if you just implement the "top 5 most requested features" you're going to end up with some frankenbeast of a language.
Use |> for the Hack proposal
and -> for F# -style.
|> is the best operator
I mean it looks exactly like JS with an additional % placeholder.
- we already have operators on values, such as +
- |> is just another operator on values
It's incredible to me TC39 would rather have this monstruosity over F# pipes which are actually pretty similar to how most pipes work and read in most functional languages including unix `|` one.
https://news.ycombinator.com/item?id=3780367
https://news.ycombinator.com/item?id=6418337
It makes me really question the judgement of the people that are working on this language, and it explains how some of the shittier proposals manage to slip in and why the good ones are misused all over the place in every real world codebase when this is the kind of guidance devs have on where to use fancy new features.
In a few years everyone is going to collectively lose their fucking mind once again and decide that ternaries are now the devil and every React component should be chock full of do expressions. I can see that coming clear as day. That might finally be enough to get me to throw in the towel if the also incoming decorator hell doesn't do it.
I was an early adopter of the original decorators proposal as well as the F#-style pipeline operators. The more time has moved on and other bits made it into JS proper, I'm far more inclined to stick to what's "in the box"... Have even considered just writing straight JS + ESM (modules). Of course, I also like JSX, though not sure of any proposals of how to get that "in the box" as e4x died on the vine and a lot of other efforts didn't gain much traction either.
Only now?
This example was literally taken directly from real-world code in the React codebase (a script to call jest-cli). While you're correct that there are probably a lot of clearer and more readable ways to write this snippet, the fact of the matter remains that people write code exactly like these "real world examples" all of the time.
Examples?
If the motivation behind these features are React components, javascript clearly lacks proper logic in array (and object) literals, like
Although it seems pretty strange to add syntax only because some specific user interface library together with a specific language extension could benefit from it.I also have a feeling this doc lists every sort of usecase, not because they are advertising it as "always the better version", but because it's a design doc that needs to factor in edge cases, such as using a pipe following chained function calls.
You cherry picked one example of a tangled/messy block of code that they used to communicate specific idea around "left to right" comprehension and flow of the data using the new syntax. For that specifically it did a fine job.
But that doesn't mean that's the way you should be writing code in the first place, given it started with a mess and only used one piece of syntax to change it.
I will admit it's a poor example to open with. But for a design doc about exploring and debating ideas it's fine.
I don't think it's fair to say it's "cherry picking" to focus on the example they focus on themselves. They have a few other examples too, but I would say "I don't see how [NEW] is better than [OLD]" for many of those as well (and for some, I think the [NEW] is significantly worse).
Plus the doc is #1 on HN after all, which could IRL push it beyond a proposal stage if done right.
If i read a code and i want to now what it will do, i wanna first read that it will output something. If that is what i search for, i read the nested code.
With the pipe style i waste more time, even if the code looks clearer.
I not overused example could look like this:
If i don't care about console.log, i don't have to read the nested code.On the other hand, pipe is less useful when you don’t have partial application of function built-in. So yeah I think this is not worth it.
Ironically, OO gets thrown under the bus a lot lately because it ain’t functional. But in reality, early OO languages like Smalltalk and CLOS were much more functional in this regard, you always had an implicit return of self, which could be chained easily.
I (over)use the piping operator (|>) in Elixir a lot. I just like the way it reads. But one thing I don’t love, is that it’s not an easy sequence to type.
I personally find pipe style much more readable.
Then again, this is my typical opinion to a lot of the proposals - I see what this is useful for but I haven't experienced enough pain to really argue for it. However, if it does make it into the spec then I will use it probably because it's there.
I also recognize my stance as one that not many people like in other contexts because it can be interpreted as me not looking to improve my situation. But on the flip side I think cluttering the language specification with a lot of superficial syntactic sugar is a mistake.
Very much this. They need a new question on the survey "does JavaScript need new syntax or can we just leave it alone and work on perf/tooling/etc. of the existing stuff without throwing a bunch of new junk in"?
X
.this()
.that()
.then()
.do()
.a()
.thing()
And I expect this to let me keep that chain going for when I need to run a static function against the chain
X
.this()
.that()
.then()
.do()
.a()
.thing()
|>JSON.stringify(%)
With pipes you can visually follow the manipulations and function calls in the order that they happen instead of being forced to scan the code inside-out & outside-in, matching parentheses and function call parameters in your head, while still visualizing intermediate results to get 1 final return value.
I find Elixir code much easier and quicker to understand, in large part thanks to its (admittedly, imperfect) pipe syntax. Code written in this way is also much easier to debug because you can quickly add `console.log`, breakpoints, or equivalent between the pipes.
I find this unnecessarily time-consuming and difficult to parse and I'd likely raise some flags in a code review:
Without pipe syntax, I'd refactor this to: But you often find yourself having to add additional logic, e.g. to scrub sensitive values, so it would probably end up closer to:personally i detest pointfree syntax. having intermediate values makes it much easier to step through code with a debugger & see what is happening. and it gives the reader some name for what the thing is, which is incredibly useful context. the enablement of pointsfree styles is one of my main concerns about potential pipe operator syntaxes: the various syntaxes that have been raised often introduce implicit variables which are passed, and i greatly fear the loss of clarity pointsfree style brings.
maybe there's something beyond the pointsfree vs not debate here that i'm missing, that makes you dislike the refactored example. personally i greatly enjoy the flatness, the step by step production of intermediate values, each of which can be clearly seen, and then assembled in a last final clear step. that is much more legible to me than one complex expression.
(1) named intermediate values are sometimes more readable ... though I have examples where it's very hard to come up with names and not sure it helped
(2) debugging is easier.
For (2) though, this IMO is a problem with the debugger. The debugger should allow stepping by statement/expression instead of only by line (or whatever it's currently doing). If the debugger stopped at each pipe and showed in values (2) would mostly be solved. I used a debugger that worked by statements instead of lines once 34 years ago. Sadly I haven't seen once since. It should be optional though as it's a tradeoff. Stepping through some code can get really tedious if there are lots of steps.
I have tools that can do it, but I'll still have a better time splitting out a variable for it, especially since what I really want is a log of all the intermediate values, so I can replicate what it's doing on paper
In languages that more easily support repl-driven development (e.g. Clojure), I think this is less of an issue. If you have a handful of pure functions, you can quickly and easily execute them via the repl, so you get a lot of clarity as to what those intermediate values look like even if the functions are ultimately used in a more point-free style.
But on the other hand, this would be a nightmare in C# (what I use in my day job). Sure, you can execute arbitrary expressions while debugging C#, but IMO you can't really achieve the same clarity. I'd rather see intermediate values like you suggest since it's easier while debugging, vs a bunch of nested function calls.
"styled" seems entirely pointless here.
This is why I'm happy to see the pipe syntax proposal, it avoids unnecessary temporary variables while simultaneously aiding readability.
The thing is I don't think it's all that much more readable. No matter which syntax you use, there's still the same number of "things" going on in a single statement.
I do have to admit I never worked with a language that uses |>, so I'm sure that with increased familiarly with this it would become "more readable" to me, but one has to wonder: just how many calling syntaxes does one language have to support? More syntax also means more potential for confusion, more ways to abuse the language/feature, more "individual programming styles", more argueing over "should we write it like this or that?", more overhead in deciding how to write something, harder to implement the language and write tooling for it, and things like that.
There is always a trade-off involved. The question to ask isn't "would this be helpful in some scenarios?" because the answer to that is always "yes" for practically any language feature. The question to ask "is this useful enough to warrant the downsides of extra syntax?" I'm not so sure that it is, as it doesn't really allow me to do anything new that I couldn't do before as far as I can see. It just allows me to make things that are already too complex a bit more readable (arguably).
Have you never worked with Bash? It's basically the same thing
One habit introduced to my current team by a former co-worker involves having even more intermediate keys than that:
In the beginning I wasn't a fan of it, as I do a lot of Haskell (and have point-free idioms on the tip of my fingers), and it's obviously unnecessary, as you said yourself. But with time I learned to appreciate this kind of function for its simplicity and consistence.Now, of course, with a pipe operator (or other similar constructions) you can get the consistency without the intermediate names.
I'm also one of those people that likes single-letter variables. I know some people hate it with a passion, but I find it very convenient. Just makes it easier to read as there's less to read.
I'm not smart enough to do Haskell, so I can't say much about that.
I agree with your remarks, there's no right or wrong, it's like tabs and spaces.
Btw I'm also not smart enough for Haskell, but it hasn't stopped me so far ;)
Given that the OP stated that they like the pipe syntax, and the refactored version illustrates the hoops you'd have to jump through without it, I guess your comment is just a strange way to agree with the OP?
I can only hope this leads to Javascript becoming even more unbearable. Perhaps only this can weaken Google's resistance to WASM.
For me the piped version seems way better, way cleaner, with cleanly separated processing steps.
I don't like nested steps because for nesting multiple arguments functions your expression tends to grow in both directions and parts belonging to the same step tend to end up really far from one another and clearly distingushes them from data that's pushed through the pipeline.
% is short so it keeps parts of the same processing step together.
This syntax also enables you to express which part is the data to be processed, what are the processing steps and what are additional parameters of the processing steps.
(the requirement being that join(' ') returns a function that takes one arg)
The middle example would be using native stuff which is why you have the wrapper function (kinda like you'd have a function wrapping a callback)
Just replace this:
With this: For the join example, you must do this: It de-sugars to: ... which of course is simply:Does join(a)(b) mean join(a, b) or join(b, a)? Does it mean a.join(b) or b.join(a)? And is a or b the delimiter? I don't really feel confident without looking it up or trying it out. I have a similar problem with Haskell's function syntax a -> a -> a.
If the function was called makeJoinerBy(sep)(array), it would be somewhat clearer:
That’s highly subjective I’m afraid.
“Take entries of envvars, turn that into k=v, join that by a space, make dim $, previous that, and args, log that”.
Personally I have no clue what’s going on at first glance with all these %%% “thats”. It’s meant to be declarative, but reads imperatively instead.
“Log dimmed $, k=v pairs of envvars, node, then args”.Pipes are probably better used on a set of operations that takes input and runs multiple functions on the input, rather than fiddling with string concatenation multiple times.
What do we use multi-pipes for in unix? I can't recall the last time I used one where the middle action wasn't a filter of some sort. grep -v to remove lines, or colrm or awk print to cherry-pick fields from a multi-field line. Once in a very long while I need to do three commands with filters between them. Beyond that it's too complicated and I make a script to handle several of the steps.
A more aggressive reformulation would be to prefix the original code with
Leaving the example being converted as simply: vs This more clearly highlights the obvious limitations of pipelines - piping envOutput but not 'node' nor argsOutput is a jarring syntax-mix here. Though I think it offers some hope for them working well in other scenarios - possibly in curry-heavy applications.Only if the first parameter of the function is the sole subject & subsequent parameters are "additional". Which isn't the case in this example: all params are equal subjects.
But you can still think about this as merging in two additional pipes into the one you are processing. So 'node' and argsOutput are just very short pipes that you are merging into the flow of current one.
Btw... chalk API feels super weird.
"Signor Rossi cosa vuoi? … E poi, e poi, e poi" (Viva la felicità by Franco Godi [1])
[1] https://www.youtube.com/watch?v=UrKKMtjNWCI
I suspect that many developers expressed their desire for this operator under the believe that it would make their style of programming easier, which F# indeed does for many code bases, particularly those that use libraries such as fp-ts or rxjs.
However the advanced version fails to deliver that
You can also cram it into one line with semicolon:
So it's basically Hack syntax for pipes with just ;_= instead of |> and _ instead of %. And you need to 'mark' the beginning of the pipeline with `let _=`Additional 'benefit' is that until you leave the scope you can access output of the last pipe through _.
You can always use ;_= for consistency and pre-experess you intent to use the piping in the current scope by doing `let _;` ahead of time:
Full disclosure, I hate all of the above but I love Hack syntax with |> and %.To better confer the direction of the pipe you might even use the letter that is oriented to the right:
And if you want to use your pipe as an expression or return it from the function , instead of ; might be better: Surprisingly semicolon auto-insertion doesn't interfere with this:Btw I think I'll name this operator duck ,D=
Maybe the pipe syntax extension will be introduced if we threaten to make duck operator a thing?
Reading this version though I also notice something else that the F# style enables that the Hack style doesn’t which is that it supports destructuring. So in F# style, I can more easily make pipe steps that pass on multiple values in a structured object or array, then access them easily down-pipe.
Is this Perl?
DROP jerkings_tab;
Is this really Perl?
Can you imagine a survey where everyone says they're satisfied with the language as-is? Of course not. That's statistically impossible. Even if 99% of the needs of developers are satisfied by a language, people are going to eventually answer that they'd like something that the language doesn't have, and that's always the case.
Let's say JavaScript implemented nearly every single language feature that's ever been invented. That is except the goto statement. Upon being surveyed on what they think is currently missing from JavaScript, a significant number of developers will have to respond with goto. Does that mean JavaScript is actually "missing" goto and that it was a mistake that it was never implemented in the first place? Of course not.
I just write things as stupidly as possible now, and just do the second one even though there are nicer "ruby-ways" to do them - maybe this is "bad" but I find it easy to read and grok...and it doesn't cause arguments during review <.<
Yes! A hundred thousand times yes!
I have not seen this syntax in JavaScript before, but just by having the vague notion that it's pipe-like, I was able to generally understand what was happening within seconds of reading it. I'd have to read up on the syntax a bit to confidently write code in this style, or perhaps to fix a bug (does "${%}" do what I think it does) but I can quite literally comprehend the author's intent almost at almost the same speed as I can read.
Note that even without knowing what chalk is or does, the flow of everything makes it extremely clear what exactly the high-level outcome is supposed to be. We're building up a string like "$ FOO=bar BAZ=qux node --arg thing --arg2 more_args". We're doing something fancy with it that I don't quite know about just yet, but the above knowledge makes it very easy to fill in that gap.With the second one, I have to construct a tree in my head
The confusion is compounded by not knowing the details of chalk. I have to jump backwards and reason about what the result of a non-linear chunk of steps is to make sure my guess is consistent.I have no opinion on the merits of this particular syntax, its implementation details, or in comparison to alternate proposals of similar ideas. I'm sure there's worthwhile debate to be had on those details. But from a high level, I'm a fan. My head is not a meat-based tree traverser and I'm guessing most people's isn't either. Human brains are big fans of linear narratives. While I love movies and shows like Memento and Westworld, this kind of disjoint storytelling is quite literally done for the purpose of generating confusion and not for promoting comprehension.
Also, there may already exist better ways of expressing this particular example linearly and succinctly. If there is, I'd probably prefer that. Worst-case scenario you can always deconstruct nested function calls into sequential variable assignments, and maybe that's the "best" answer here instead of new syntax. But is an approach that can be understood linearly from start to finish clearer than one that requires mental tree-walking? Yes. Yes yes yes yes yes.
However, the 5th most requested feature in that poll is somehow "functions", and "sanity" also appears in the results, so this particular source may not be a good one.
You can even use an array if you don't need to peek the value in the middle of a chain of operations
Example:
In each step, you can name the intermediate result accordingly to it's meaning, should be more readable than calling it %const oned = one(value);
const twoed = two(oned);
const threed = three(twoed);
This proposition goes out of its way to find problems with code that is written in a confusing and uncommon way in the first place.
Code is read far more than it is written. Use temporary variables. Put in the effort to name them once, and then that effort pays back every time anyone needs to read and understand the code.
There are lots of cases where they’re not and are just noise written in exactly the style of the original comment. Or worse all the one-shot temporaries get assigned to the same worthless name.
Unless you live and die by the mantra that no expression can have more than one period, pipes pretty much just bring attribute/method chaining to arbitrary functions and expressions.
I just don't think that this is always true.
Consider:
I don't see how this is better: And you can add helpful comments to pipeline code if needed: More generally though, I don't see why forcing everyone to write out intermediary names all of the time leads to more readable code. If it's more readable to do so, I will. If a pipeline is more readable, why should we be prevented from using it?Case in point:
> you can add helpful comments to pipeline code if needed
The pipeline with explanatory variables explain to you what the steps are with code. Using pipeline you need to add comments to explain "what" you are doing.
Furthermore, I'd wrap all of those lines together into a getHighestScore() function as well. That makes the complexity exactly as visible or as abstracted as you want at any given moment. The name of that function tells you what the aggregate of the operations is doing, and you can go look inside that function if you want to see the individual steps.
Explanatory variables help understand the steps of the process, this |> operator is receipe for unmaintainable, expedited code.
To me this is both concise and readable:
In the JS ecosystem, idiomatic nested function calls are read from the inside out. And in most cases that is highly readable because a single layer of nesting is all you need.
This proposal flips that on its head, so you have to retrain yourself to read code in a new way based on the presence of a |>. That adds significant cognitive overhead, especially in code that isn’t well written or that is already complex.
Your example looks nice, but also just as nice is the .then() syntax you could have used.
Now we’ll see code bases with both styles. You’ll have some team members who love pipes so much they’ll never call a function the old way doing x |> console.log and the rest of the team doing console.log(x).
Sometimes, limitations are a good thing.
Without something like that, trying to add error handling to the things which may blow up would instantly turn it into gibberish. Every single function here can fail (the HTTP request could fail, the data returned could be unparseable as JSON, or not have the right format). We should be trying to make our languages encourage us to write code which can handle those errors naturally, rather than encouraging us to write fragile code.
[0] https://fsharpforfunandprofit.com/rop/
Missing String.titleCase ? Write your own!
Personally, I don't use classes much, but sometimes I think free functions are a little too hard to find, so I tend to experiment with the following pattern.
Which makes finding an operation for a certain type easier to find (just write User and trigger autocomplete).The alternative is of course having renameUser (or userRename) and getUserDisplayName (or userGetDisplayname). The prefixed version would make autocomplete easier also.
“hello world” |> titleCase (%)
I'm starting to like the F# proposal more now.
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.
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.
It could at least be
Although if we had function currying, the convention in ML languages is to put the most-commonly-piped-in param last for these functions: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:
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:
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.
> 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.
and this
Which apparently you believe should beIn 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?
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.
... I further suspect that their problems are mostly self-inflicted, but maybe I'm wrong about that.
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.
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.
Excruciatingly contrived, but does this sort of arithmetic work in the Hack syntax? I'm genuinely curious, couldn't find any mention of modulo (or remainder) in the proposal.
Gross
What if I need the value to be replaced multiple times, like this:
I'm surprised they aren't going with an idiom like `$1`, `$2`, etc or something like in other languages that have "magic" lambda parameters.Hopefully not because there’s no reason to: in the same way you can use + or - as prefix or infix, % as value and % as binary operator are not ambiguous.
should not be an issue, though it’s useless and not exactly sexy looking.> I'm surprised they aren't going with an idiom like `$1`, `$2`, etc
That makes no sense, $1, $2, and $3 are different parameters.
Using your example,
makes absolutely no sense.Not to mention the very minor issue that $1 is already a valid JS identifier.
> or something like in other languages that have "magic" lambda parameters.
% is one of those, it’s what closure uses for its lambda shorthand. Scalia uses `_` and kotlin uses `it`.
The latter two are ambiguous but I guess since pipes are new syntax there wouldn’t be a huge issue making them contextual keywords.
Most language with “pipelines” are curried so it’s not a concern, and in the rest it tends to be a fixed-form insertion, so it’s quite inflexible, but in both cases APIs are designed so they play well with that limitation e.g. in curried language you’d have the “data” item last (that’s obviously in Haskell), which also allows for partial application in general, while in “macro” languages, well, it depends where you decide the magical argument should be inserted (IIRC in Elixir it’s the first, so functions written for pipe compatibility should take the main operation subject first).
Clojure is cool because it has both plus a macro where you give it the name of the substituted symbol. However being a lisp the pipe macros are still prefix, not infix.
That's not what I was saying. I was saying that using `$1`, `$2`, and `$3` _would be different parameters, which would be good at helping disambiguate_.
That would enable this, from my example:
while also enabling something like this: ...whereas just sticking with the single `%` means you _can't_ disambiguate, and that if they instead try to allow disambiguation by deciding that the first `%` is `$1`, and the second `%` is `$2` (and so on), then now you can't use the template-string example I gave.Having unique "magic scope variables" at least allows you the flexibility to handle non-unary use-cases.
Either way, this is another case of "every lexer/parser has to be riddled with special cases" to handle "is this `%` a fancy-pipeline-identifier, or is it an operator?"
So the same thing except more verbose.
> while also enabling something like this:
Enable for what? A pipeline threads a value through a sequence of operation, there is no second parameter.
> ...whereas just sticking with the single `%` means you _can't_ disambiguate
Which doesn't matter because there is nothing to disambiguate.
> Having unique "magic scope variables" at least allows you the flexibility to handle non-unary use-cases.
Which do not and can not exist.
And even if they did (which, again, they don't), you could do exactly what Clojure does with its lambda shorthand: %1, %2, %3, %4.
> Either way, this is another case of "every lexer/parser has to be riddled with special cases" to handle "is this `%` a fancy-pipeline-identifier, or is it an operator?"
There is no special case, having the same character be a unary and binary operator is a standard feature of pretty much every parser. Javascript certainly has multiple, as well as operators which are both pre and post-fix.
I’m a little torn because pipes might be pretty nice, especially for prototype code and small projects. One thing this proposal doesn’t acknowledge is that for production code, deep nesting’s often impractical and often considered an anti-pattern in the first place, so making it easier isn’t a common need or problem to have in my experience. Usually I’m going the other way, having to make it more tedious. Using temporary variables and breaking apart nesting is, far more often than not, necessary in order to do proper error checking, and just to make code readable, commentable, and refactorable, etc. I feel like what we need is not a way to make deep nesting easier to read, it’s a way to make temporary variables less tedious, perhaps while also piping from one function to the next… that would be really helpful in a deeper way than just adding another chaining syntax. Is the syntax is stuck at “|>”? I guess it’s not possible to override bitwise-or (“|”), but “|>” feels maybe a little clunky?
Yes, there are some limitations in comparison to Hack Pipes. But those are far outweighed by not messing yet again with the language’s syntax.
In absence of |> % syntax I'd nearly always would go with intermediate temporary variables instead of pipe(). "point-free" syntax feels horrible for me and wrapping everything in lambdas feels excessive.
In order for TypeScript to check if the return value of one function has the correct type for the next function, you need to use generics, but generics don't allow for a variable number of generic parameters, so `pipe` would just have to use unknown or have dozens of overloads for each length of pipe usage (within reason).
I know TypeScript is a different, optional language, and I see no reason why they couldn't add the feature without it being in JavaScript, but that's not generally how TypeScript operates. If JavaScript doesn't add it, TypeScript won't. There's also lots of tooling that will type check your JavaScript when available, which will benefit from a simpler model.
We know so much more about how to create programming languages today than we did then, and the whole "Year of the Linux Desktop" has become a WebAssembly meme now every year since its introduction six years ago, with it getting popular always next year, next version, with feature XYZ. Seemingly creating unmaintainable/debuggable mess from external languages with no true 1:1 into WebAssembly isn't as big of a hit as the originators expected.
Yet every time someone asks why there hasn't been movement here it is "Year of WebAssembly is next year!!!" WebAssembly has managed to slow actual progression towards something good. With the browser monoculture you'd think it would be easier now than ever to start a fully integrated second language with WebAssembly compilation for backwards compatibility.
Stuff like that takes time though, so if you're antsy, you have two options: get involved, or wait patiently.
Yew (Rust) is one that I've been following with interest, the hello world examples are interesting enough. I know people that have been liking the direction of Blazor (C#) more, but it has a larger initial payload, that I don't care for, it's also closer to SSR approach in the browser.
The problem with both, IMO, is that they don't have a good UI library/toolkit. I find mui.com (with React) pretty much the best browser ui framework I've experienced (since 1996). To me, the first language + component library that targets WASM and that level of components will likely win.
Flutter is probably the closest example I'm aware of, though afaik it's JS as the browser target, not WASM, but wouldn't be surprised if this changes assuming DOM interop for WASM becomes better performing.
As for GP, you're right... there have actually been MANY attempts at other languages for in-browser. In the end, the JS runtime has been hardened and all other roads have led to WASM being the second target. I still remember a lot of people using VBScript in IE. I was an outlier in that I used JScript for classic ASP.
In the past you had Java applets, Silverlight and Flash. Compile-to-JS languages depend on rather than replace JavaScript, but it's about as close as you can get without being a browser developer. Chrome did originally intend Dart to be supported natively in the browser but eventually gave up that effort. Perhaps now Chrome is dominant to push it through without getting buy-in from Apple and Mozilla, but I doubt they have any interest now that there's WebAssembly.
Like it or not, WebAssembly is seen as the answer for additional languages in the browser. And for most developers JavaScript functions well enough as either a development language or compilation target.
The DOM in IE was essentially a COM API and callable from any supported scripting language. This included JScript (their JavaScript clone) but also VBScript and PerlScript.
Personally, I do think JS is a better fit than Rust or C for almost everything people do on the web. And the support in other languages is still too bad to use. But you can use it whenever you want, differently from a second language you may invent.
Something "simple" like:
Turns into the following:g() |> f(%) |> d(e(), %) |> a(b(),c(), %)
Which makes super clear what processing is actually done. Which is the data to process and which are just parameters of processing.
Because it could equivalently be:
e() |> d(%,f(g())) |> a(b(),c(), %)
If the data you process is rather produced by e() not by g().
This new syntax allows you to express intent beyond what's possible without it.
a(b(),c(),d(e(),f(g()))) is just function call soup.
If it's really good rule you can advocate for it at this stage. It might be prudent to use |> % only inside function call parameter or possibly as right hand of an assignment instead of everywhere where parser expects an expression.
a(b(),c(), g() |> f(%) |> d(e(), %))
Although I'll be honest, I don't like it. Other parameters of a() call for me occlude the flow and the intent.
We could make it a bit better with newlines.
But if a() takes more parameters after the main one then we have the same problem as usual where parts of the same call can end up far away from one another. For me is still better and I wouldn't want it to be prevented by language syntax.Also shout out to bash pipes