The problem with Javascript IMHO is that it's difficult to say "enough". As someone who uses it everyday and loves it, I believe it's in a decent place now, but it keeps adding more and more strange operators that virtually no one uses or has use for in everyday applications. Things like Symbols, BigInt, etc. are way too rare in JS and only useful under certain "ideas of workflows" to be worth being part of the core since they introduce a whole incompatible layer of dealing with things and don't mix well with the rest of the language. I'll even throw "classes" and "generators" to the list of things that were a mistake, as time has shown.
This is the same with the pipe operator, we already have arrow functions, thenables, and TWO different types of .pipe(), which makes syntax a lot cleaner when needed. IMHO adding this pipe operator "|>" would be a mistake, it only complicates the language making:
- Browser competition harder, when the language is really broad and difficult it makes it harder to create a new engine.
- Mastering Javascript harder, there's a lot broader amount of concepts there which are not really useful except.
- No real benefit as I explained. For some people's worldview it's better, but for us the rest of mortals[1] it's just overhead.
A really good indicator of whether something is useful in core JS is: has someone made it into a library or sublanguage[2] that is widely used? Or a language feature? Promises and events def passes that test, while Symbols, classes, generators and this new pipe operator definitely don't. Bigint is arguable, since it passes the test but remains fairly fringe use-case in JS.
[1] Note that I'm the first one to experiment with JS and try weird things! I'm just saying that these should remain broken experiments on my computer, or nice libraries in npm, but not part of the specification that no one uses.
I think it's important to separate two major categories of recent JS additions:
1) Advanced features that cannot be replicated without runtime support
2) Syntax sugar
Symbols, BigInt, and generators are the former. Pipes and classes are the latter. (1) gives library authors access to low-level primitives that can allow them to do things that were literally impossible before (I've gotten a lot of use out of Symbols, personally), but they aren't often used directly in application code. (2) are usually more application-facing, and may or may not be a big enough quality of life improvement to justify their existence.
I guess what I'm saying is: just because you haven't seen lots of Symbols or generators in the wild doesn't mean they aren't having an impact, or weren't worth adding.
To add to this point, it’s quite likely that the vast majority of JavaScript developers are using generators and symbols, whether they realize it or not. The reason is that your build system of choice will typically have been compiling async/await into a combination of generators and iterators for years now.
It’s probably getting to the point where it’s no longer necessary in a lot of cases, but these constructs that “nobody uses” were able to implement the newer and more widely features long before they were available in browsers.
There's libraries to work with BigInt without "runtime support". Arguably Symbols and Generators cannot get there 100% of their features without runtime support, but for the vast majority of usecases out there (Symbols as a unique keys, generators as a stream of data) there are many ways of doing that with JS without the need of these constructs that are just as clean. For instance, Symbols solve such a very edge case (key collision) that IMHO it's def not worth creating a whole new abstraction for it.
> they aren't often used directly in application code
The problem for me with this is that abstractions leak, and suddenly you are wondering why you cannot add two simple numbers or why there's a 3rd level of hidden properties in objects.
The teams working on evergreen browsers seem to be the more effective place for vetting the continuum between and including 1 and 2--they do remove features. Certainly better than the various experiments across the Internet (including the black hole npm).
I think the base pipe operator by itself is nothing but a win; I think it's pretty clear and unambiguous even if you aren't familiar with it, and it makes certain cases much more elegant. I don't really see any downsides to it, except maybe that it has limited usefulness on its own.
My feelings about the "Hack pipe operator" are much more complicated. I can see how it dramatically expands the usecases for piping as a whole, given JavaScript's other norms and constraints (no automatic currying, lots of method calls). But I also think it tangles up control-flow in weird ways that could get really hairy fast. And at some point, when you're no longer "just piping" into a unary function, are you really gaining much benefit over the normal function call syntax?
As someone who's used the pipe operator in a different language in a professional setting, I can attest that it makes code a lot easier to read and write.
I know pipe is popular in other languages and on Linux, but all of the examples on that page look contrived, and seem more like a justification after the fact.
If I had to pick, I'd choose the more limited F# style, it's easier to understand and imposes some sane constraints around usage.
No more JS code golf, brevity in itself should be a non-goal
What a syntactic mess. JS is slowly turning into C++. I've already complained about how spread[1] syntax breaks common intuition in certain use cases, I haven't even had time to keep up with all the insane operators they've been adding.
Personal opinion: none of the syntax or results in your blog post were confusing, other than square brackets looking confusingly like curly braces in the font. It’s how splat works in every language I’ve used. “This operation that doesn’t make sense doesn’t work” especially not confusing.
I read that fine the first time. It’s exactly the behaviour I would expect and would have specced. Splat as an actual operator doesn’t make sense; it’d have to produce some kind of horrible magic value.
Engines might be able to improve the error message, but I can’t say I’ve ever run into that one.
> I get that you're being purposefully obtuse or whatever
Fuck off. :)
> but in just about every sane language (not to mention plain-old mathematics), extraneous parentheses don't behave like this
Sure they do. They imply their contents is a value (which a splat emphatically should not be), rather than part of the syntax of an argument list, pattern, or literal. Now, the existence of splat syntax implies a dynamic language, which means it’s never going to be fully sane, but I can say I’m extremely surprised to find that `f(a, (*b))` works in Python. It doesn’t in Ruby. `f((kwarg=value))` doesn’t work in Python.
> Try to put yourself in a neophyte's shoes, this would confuse the hell out of you.
The error message, maybe. Like I said, I never ran into it. The behaviour of the syntax, no.
package main
import "fmt"
func main() {
a := []interface{}{"hello", "world"}
fmt.Println(a...) // OK
fmt.Println((a...)) // ERROR
}
Lua doesn't like extra parentheses:
a = {"hello", "world"};
print(table.unpack(a)); -- OK
print((table.unpack(a))); -- Only prints "hello"
Perl does like extra parentheses (but then again it doesn't really have a special "splat" syntax and just flattens all lists everywhere):
my @a = ("hello", "world");
print(@a); # OK
print((@a)); # OK
You seem to want `...` to behave as an operator, but it really can't since it doesn't produce a value. If you think it *should* produce a value you then have to decide what the type of that value should be and there doesn't seem to be a good answer for that. I mean, should this be allowed?
let a = ['hello', 'world'];
let b = (...a); // What should the type of `b` be?
// Somehow equivalent to console.log('hello', 'world')?
console.log(b);
I suppose you could introduce some MagicFlattening type into the language and make `b` be of that type. Then every time any function is called the interpreter would have to check and see if the argument is a MagicFlattening object and, if so, change the call from having 1 argument to having N arguments. This would make things slower and make creating a JIT more difficult. It would have unfortunate ripple effects across the entire language. You've now got these magic objects floating around that can unexpectedly change the meaning of any function call in your program. This is probably why most languages have chosen not to go this route.
Instead `...` is a different way of calling a function. You can call a function in the normal way with `(` and `)` and you call it in this alternate way with `(`, `...`, and `)`. Any given call site only uses one or the other of these ways and which one it uses is known at compile time so there's no need for runtime checks and no magic objects.
The `...` is part of the function call so wrapping it in extra parentheses doesn't really make sense. You wouldn't change `console.log(a, b)` to `console.log((a, b))` and expect it to work the same, would you?
First off, you're plain wrong about Python, so I doubt the veracity of the other examples (particularly in languages I'm not an expert in, like Ruby). But either way, you (along with the other folks commenting) are missing the point entirely, so I'm not sure you're actually familiar with the language spec nit I'm criticizing.
Why `console.log` fails on `((...x))` is not because the function can't take arrays as arguments (in fact, it can: `console.log(["hello","world"])` works fine), but rather because it's running into another spec: inline arrow function notation, i.e., `(...x) => true`. So the spec (because it's so confusing) thinks I'm trying to define a function.
That is the problem here, not the splat.
> You wouldn't change console.log(a, b) to console.log((a, b)) and expect it to work the same, would you?
Honest question, are you aware of the comma operator and how it works?
> Why `console.log` fails on `((...x))` is not because the function can't take arrays as arguments (in fact, it can: `console.log(["hello","world"])` works fine), but rather because it's running into another spec: inline arrow function notation, i.e., `(...x) => true`. So the spec (because it's so confusing) thinks I'm trying to define a function.
What does taking arrays as arguments have to do with anything? Are you suggesting `console.log((...x))` should be equivalent to `console.log([...x])`?
`(...x)` doesn’t fail to work because some kind of syntax conflict with arrow functions[1]. It fails to work because it’s not defined in the language, and it’s not defined in the language because it doesn’t make sense. You’re the only person who seems to think it does.
And if you want to use parentheses for readability, you should write `...(expr)`, `*(expr)`, etcetera.
[1] f((a = {})) and f((a = {}) => {}), for example, are both valid expressions where the inner parentheses mean completely different things, analogous to the ellipsis.
As pointed out by minitech, this syntax was allowed in some versions of Python 3 but that was a bug which has since been fixed. I didn't run into this bug in the version of Python 3 that I used for testing so I had no idea of the bug's existence.
> Honest question, are you aware of the comma operator and how it works?
I am. The existence of the comma operator is the reason that `console.log((a, b))` parses (and means something different than `console.log(a, b)`). If we consider a hypothetical version of JavaScript which does not have the comma operator then `console.log((a, b))` wouldn't parse at all! The comma there can't be the comma operator because we've removed that. And it can't be part of the function call because then the first argument to the function call would have to be `(a` which is obviously not a valid expression. And it can't be part of an array literal or an object literal because it isn't inside of `[]` or `{}`. That leaves us with no valid ways for this to parse.
You can usually add extra parentheses around any expression without changing the meaning. But "all the arguments to a function" is *not* an expression (except in the case where there happens to be exactly one argument). This is why it doesn't make sense to change `console.log(a, b)` into `console.log((a, b))`, why it doesn't make sense to change `console.log()` into `console.log(())`, and why it doesn't make sense to change `console.log(...a)` into `console.log((...a))`.
This feels like a massive break with the most fundamental syntax of the language. The examples might look more readable in isolation, but I feel this would reduce readabilty in practice by making the language more complex to read and for allowing massively mononolithic expressions, like the ternary operator.
I like it. Anyone who’s written complex Excel formulas knows the pain of those deeply nested function calls. For those scenarios this is much easier to read IMHO.
A pipeline operator would be nice, but I just took a look and I agree that |> and % are difficult to read. I also think |> is hard to type, which makes it more painful.
Admittedly I don't have a stellar solution to this though. Only alternative I can think of is ~
It is a unary operator, from my understanding of operators, as it only operators on one subject (~2). However, I very much doubt TC39 would use this character, as it would cause a great deal of confusion and poor semantics, not to mention the complexity on the engine side (does the user want NOT or pipe?)
This example of nested function calls needs to be read from the inside out (execution is f, g, then h) which is unintuitive to how we read and parse text.
The alternative is to use intermediate variables instead of nested function calls to represent the control flow of f, g, h.
But intermediate variables are almost always better if named properly, as they provide a contextual name as to the purpose of the output of the function. Then only very minor downside is a bit more typing and more lines of code.
You forgot the biggest downside which is that naming things is hard. It also often causes important names to get lost in the jumble of temporary names.
IMO, I think this is far less readable than the pipe operator. Besides that how many times do you need to call three functions without additional parameters?
Stylistically I think it is most appropriate when you are writing conversions/cleanup/transformations. Places where method chaining is nice but not always possible. Suppose you are doing a bunch of string manipulation with custom string operations (maybe a custom regex engine with timeouts unlike the default JS regex API).
TC39, as the article notes, and where the F#-style pipe has been shot down enough that it's been abandoned in favor of the Hack-style one by those championing a pipe operator.
We could also have used the pipe operator like this:
const testPlus = () => {
assert.equal(3+4, 7);
}
|> (%.name = 'Test the plus operator', %)
;
I don't really see the point in general purpose chaining. I don't really see the point of flatmapping tons of nested function calls. Instead we can abstract it and start another nested stack if necessary. I thought we moved away from utilizing promises towards async await because of the chaining issue. It feels like it's being repeated.
I don't think it should have been written like that. The Object.keys().map().join() should've been assigned to an appropriately named variable before being used in another function. There are better ways to avoid writing code this way.
This is the same with the pipe operator, we already have arrow functions, thenables, and TWO different types of .pipe(), which makes syntax a lot cleaner when needed. IMHO adding this pipe operator "|>" would be a mistake, it only complicates the language making:
- Browser competition harder, when the language is really broad and difficult it makes it harder to create a new engine.
- Mastering Javascript harder, there's a lot broader amount of concepts there which are not really useful except.
- No real benefit as I explained. For some people's worldview it's better, but for us the rest of mortals[1] it's just overhead.
A really good indicator of whether something is useful in core JS is: has someone made it into a library or sublanguage[2] that is widely used? Or a language feature? Promises and events def passes that test, while Symbols, classes, generators and this new pipe operator definitely don't. Bigint is arguable, since it passes the test but remains fairly fringe use-case in JS.
[1] Note that I'm the first one to experiment with JS and try weird things! I'm just saying that these should remain broken experiments on my computer, or nice libraries in npm, but not part of the specification that no one uses.
[2] Coffescript, JSX, Typescript, etc.