A proposal for adding a pipe operator to JavaScript
github.com
github.com
Furthermore, I'm continually astounded by the naysayers who are opposed to language evolution as a general principle. I read these sort of objections all the time and all I can think is that if we took the "change almost nothing about our tools - think of the juniors!" approach advocated by these folks, we'd still be writing C and the industry would be an order of magnitude smaller than it is. Software development is a unique field of endeavor where it is remarkably easy to build and then use new tools, and while it does lead to lots of stressful change, it also leads to our discipline accomplishing more and more as the years pass. Thankfully, the silent majority seems to understand this just fine, which is why despite the tenor of the conversation around these (and other) parts, our tools do continue to get better and better as the years go by.
I’m not convinced temp vars are avoidable in production code, since usually you end up needing error checking/handling on intermediate results anyway. Teams I’ve worked on generally try to avoid dense nested and/or chained expressions for this reason, but also because too much density or conciseness also limits the readability/reasonability of code.
I like that pipes turn nested expressions right-side out like the article points out, but temp vars already do this too.
The other thing that temp vars help with is making it easier to change functions in the middle of a sequence. If you have a nested expression, sometimes having to refactor something in the middle leads to a bigger avalanche of changes than necessary. Using temp vars might be somewhat less churn in some cases due to already haven broken down the expression into pieces. Maybe this pipes design will allow that too, it’s hard to say without using it for a while.
While I’ve been on the team pushing for pipes and for the Hack variant, I do appreciate that there are many instances where giving an expression a name is helpful.
This is a tool that comes with great responsibility. My hope is that people will learn how to use it. It’s worth it imho the added language complexity.
Not that I think this will hurt much, as the language and community fled that barn long ago. There are already 50 ways to do most things in JS, half of which will be present in your deps, and only 5 of which a typical active JS developer will happen to have stored in their brain at any given time.
I love a good lisp or ML, and all things being equal, less syntax is probably better than more. But flexibility is also a good thing, and the irony is that the proof is almost always in the pudding - if a language gets too complex (e.g. Perl, C++), it is almost always true that the community eventually establishes solid guidelines around which idioms to avoid (or when to avoid them). Sometimes there's pain, but rarely do languages die because of it.
I think it's possible to have a reasonable debate about when to introduce new language features, but around here, in my opinion, the pendulum has swung very far toward the "don't move my cheese" side of things.
We’re aware of what’s happening, or has happened to JS, and we’re having the realistic discussion about this stuff finally. The once ‘oh neat, new JS thing, let’s use it’ phase is over. We’ve seen the concrete impact of too much shit layered on top of each other in actual workplace codebases.
If you wanted this, you shoulda came earlier, the party is finished. We went too far between 2010-2020 and we’re all exhausted. It’s got nothing to do with ‘don’t move my cheese’ and everything to do with ‘I think we’re good over here, thx’.
We need to take a step back, evaluate all the nonsense (clean up after the party), before we can throw another one.
Like, it’s all good F# pipe people, totally appreciate your input, but we have our own clusterfuck that needs to be sorted out at the moment, maybe next time.
Yes, but this goes to the parent's point. Natural languages dialects can be so distinct that even though two interlocutors may be speaking the same language (e.g. English), they still might not be able to understand one another.
Classic example: https://www.youtube.com/watch?v=73d6h_go7QI
This happens in computer programming languages as well. If you're a C++ programmer who uses smart pointers for everything, then you will have a rough time with my C++ code that uses raw pointers for everything. Even though we would both call ourselves C++ developers, our code follows different idioms, they are essentially different "dialects" of C++. I put dialect in quotes, because I wonder if there is any effort to track, catalogue, or otherwise create a taxonomy of computer programming language dialects as is done for natural language dialects.
Like, you can figure out where someone was born based on the words they use. Can you figure out what kind of projects a person has worked on, the companies they worked for, or the school they went to based on the style of code they write? This would be the job of a linguist or sociologist I think, and I don't know how often they look toward or communities for study.
Hmmm, kinda.
The pipe operator enables more readable code, so whilst I agree there would be more ways to do things, the average code readability could well go up.
Every time one of these things is introduced, 90th percentile code gets cleaner and 50th percentile code gets worse.
All that seems to be being accomplished -- especially in the JS world -- is reinventing the same things over and over again in some sad attempt at "modernity", but using increasingly more resources to do so (and in the process, irritating more users and even developers.)
and the industry would be an order of magnitude smaller than it is
It's already too big, having traded quality for quantity far too much. I get the feeling that a lot of developers seem to be stuck in some weird "bubble of positivity" that makes them immune to negative feedback and think they can do whatever they want and users will love them for it, but if you talk to the users, the reality is quite different.
As the old saying goes: "It's not the tool, it's how you use it." If we stop trying to constantly create and promote new tools, maybe developers can start actually getting good at using the existing ones, and only then will quality go up.
Weird, when I started programming I wished there were a pipe operator and I never understood why it didn’t exist in most languages.
I don't think that's what those people mean. What those people mean is "if you want something else than JS, use something else than JS". If you want pipes, you can already use OCaml, ReScript, F#. They compile to JS and work well.
> Thankfully, the silent majority seems to understand this just fine, which is why despite the tenor of the conversation around these (and other) parts, our tools do continue to get better and better as the years go by.
Which is also why brain power is lost of things like this or the walrus operator in Python. JS is a fine compilation target. If you really like |> and JS, you can use a babel plugin! I don't see the point of adding it to the base language.
Maybe if people stopped always using the most popular option available, we wouldn't need to constantly add stuff to this popular option, and could have a rich and diverse ecosystem.
And as far as I know, this will be in a future version of ECMAscript, which means that most people will in fact be using this via babel?
> And as far as I know, this will be in a future version of ECMAscript
This may be in a future version of ECMAscript. Stage 4 proposals are the one accepted, stage 3 are almost official. This one is stage 2.
And, well, yes, I realize we're talking about a future maybe-feature. That said, I assume you're granting my counter-point about how, no matter how you slice it, this is still ultimately "just a babel plugin"?
My point is that there's no "clearly superior language" compare to JS. Some people want a ML flavor, some people want a C# flavor, some people want a Lisp flavor. I wish people would just use those language and improve the interops instead of putting everything into JS.
Furthermore only 7% of respondents answered that question at all, so the real takeaway is: 0.7% of survey said "add pipes" and 93% said "please don't add anything it's fine as is just leave it alone we don't need to be the next Python". This isn't convincing data in the slightest.
As for their actual example... why change it at all? I think any reasonably adept JS programmer can look at their representative "console.log" example and immediately tell whats going on. Heck, it's using the same semantics as every other C language, so really basically any programmer can look at it and immediately tell whats going on. Contrast to their "proposition" where suddenly everyone in the world (minus the Hack folks I guess) needs to take time to learn what the new weird looking symbols mean.
1: https://2020.stateofjs.com/en-US/opinions/#missing_from_js
Functions being even on there is laughable. “I have 0 familiarity with the language but I must give my opinion!!!”
And sanity… sanity is last on the list.
I could not disagree more. Temporary variables as you perform transformations are great because they force you to verbalize what it is that you're actually doing. Descriptive naming is hard, but in contexts like this it's an actual godsend for anyone who isn't the person writing the initial code. I think the use cases are not convincing, I would much rather this be a Babel plugin than a core language feature.
"string"
|> capitalize(%)
function capitalize(str) {
// do the thing
}
Temporary variables are certainly really tedious in many situations: const string = "hello"
const capitalizedString = capitalize(string)
const paddedString = leftPad(capitalizedString, 5)
I'd have a very hard time buying any argument claiming naming those variables adds any kind of clarity to the reader or writer over: "hello"
|> capitalize(%)
|> leftPad(%, 5)
But then, does _JavaScript_ need this? I dunno.In that situation, one shouldn't use temporary variables (or at least too many of them), but rather just chain the functions
const paddedString = leftPad(capitalize("hello"), 5) wrapTag("p", capitalize(parseTemplate(sanitize("hello, %s"), "Sam"))))
This is a contrived example in terms of the actual functions being called, but it's not out of the question to have at least this many transforms (just think of a method chain of more than two or three operations if you are coming from an OO point of view).My main reason for replying to this thread is to counter this idea that pipes are inelegant—this simply isn't true. At the same time, I personally don't care if they make it into JS or not.
edited for clarity/grammar
wrapTag('p', capitalize(parseTemplate(sanitize('hello, %s'), 'Sam')))
'hello, %s' |> sanitize |> parseTemplate(%, 'Sam') |> capitalize |> wrapTag('p', %)
And with some little adjustments with currying even those % placeholders can go away.It might seem like pipe is for unix geeks, but it's good enough for designers too. See Django's template system.
https://docs.djangoproject.com/en/3.2/misc/design-philosophi... https://docs.djangoproject.com/en/3.2/ref/templates/language...
As such, it does add to readability.
Descriptive variable naming _is_ hard, and unlike function and parameter naming, it is avoidable in many cases! By naming a temporary variable, you ask other developers to consider the possibility that you might be doing multiple things with it - not doing so allows a simplified read with confidence that this is a pure function pipeline with no unrelated side effects.
Naming is a great and hard and valuable thing - temporary variables are only good inasmuch as your readers habitually expect them - and habits can be changed, thank goodness!
I do agree in general that this operator isn't really needed. Probably not worse than Python's := or structural pattern matching, it's one of those things where either you base the language on it, or you don't and adding it is not really necessary.
This is incredibly important because so many developers don't use ways to describe the intent of what they are writing at all. It's bad enough many believe the swill that "the code is the documentation", but to then add new ways to make code less self descriptive is... worrying.
While I marginally prefer F# pipes, both will drastically improve the readability of JS code.
With a lot of stdlib using functions / methods of higher arity, I suppose the Hack version will lead to shorter and easier-to-read pipes in most real codebases.
As I said above, however, I think either method will be a fantastic addition to JS.
While many functions have an argument that is clearly "primary", some just don't. I personally find placeholder to be a better option, especially if used with named arguments.
Seeing your later comment about functional languages, I think they suffer from lack of overloading. E.g. F#'s fold2 would be much better as |> fold(a, %, func). Sometimes there's just no way around it.
// A. Status quo (as per the proposal)
console.log(
chalk.dim(
`$ ${Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')}`,
'node',
args.join(' ')
)
);
// With pipes (from the proposal)
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
// A normal person who is able to write good code (me :P):
const envVars = Object.keys(envars).map(envar => `${envar}=${envars[envar]}`).join(' ');
const coloredText = chalk.dim('$ ' + envVars, 'node', args.join(' '));
console.log(coloredText);
I'm not seeing it... can someone help me understand? Why are their so many proposals trying to force alien syntax into JS from "functional" languages?That is, the first place I really encountered code like this was when Java introduced streams [1]. I wasn't used to programming like this when I first experienced this code, and at first I found it harder to read; it felt obfuscated.
As I got more used to it, though, I started to love it. There are tons of operations in programming like this, where you're really just performing a successive series of transformations on a set of data. Obviously functional programmers are familiar with this, but this is also the paradigm for shell programming, where you're just piping stdout to the next small little program in the chain.
The thing is, now I really feel a bit of a "switch" in my brain when I read code like this. That is, whenever I see this, I'm just like "OK, doing a whole bunch of data processing from some input set to a final output". Over time, when done correctly, I actually find this easier to read, with less verboseness, because my brain already has tons of context about what's going on.
1. Some Java streams examples, https://winterbe.com/posts/2014/07/31/java8-stream-tutorial-...
The first answer, tedium, is unconvincing, because pretty much anyone with more than a couple of years of experience in software development in a team knows that ease of reading beats ease of writing.
The second answer, unexpected mutation of temporary variables, seems to point to a deeper issue in JavaScript. That of const not quite being const. Unless you Object.freeze everything.
The third answer, that temporary variables require sequences of statements instead of a single big expression, is more of a feature than a bug, in my personal opinion. Statements allow the reader to comprehend the algorithm one step at a time instead of trying to untangle one giant expression. To say nothing about the fact that a sequence of statements is way more debuggable.
I'd say that JS is moving closer and closer to Perl in its write-only-ness.
[1]: https://github.com/tc39/proposal-pipeline-operator#temporary...
Hah, I dunno, man. I guess it depends on how one defines "readable".
If having layer upon layer of indirection through inheritance, dependency injection, procedures split into tiny functions written in no particular order, and magic strings/numbers are considered readable code, then yeah, I suppose most mid-level developers prefer "readable" code.
> The third answer, that temporary variables require sequences of statements instead of a single big expression, is more of a feature than a bug, in my personal opinion.
Simplicity is much more important than even readability, though readability is still important. The more tools we add, the more rules and complexity need to exist around them, all while possibly adding little meaningful benefit.
In all honesty, I think that the most meaningful thing we got out of >=ES2015 is modules, promises and async/await, and maybe constants. Everything else became window dressing around existing constructs but introduced more complexity while also creating more rules. Things like classes were mistakenly implemented to satisfy Ruby and Python developers and to assist transpilers. The more I got away from using inheritance, the more I realized how unnecessary class syntax is. If you can just write a function that takes data and spits out data, then there's hardly any need for other class syntax features. If something seems to need inheritance, chances are you could have written your code to be more composable.
The more we try adding to JavaScript at this point, the more people are going to be encouraged to do things that make themselves feel clever to the exclusion of writing code that future devs won't have to spend hours deciphering.
A huge part of my job is deciphering. I don't know if it's like that for anyone else, but there's so much of it that it really makes me wonder how many millions, perhaps billions of dollars are wasted because engineers are trying to be clever.
> Indeed, the benefits of method chaining are so attractive that some popular libraries contort their code structure specifically to allow more method chaining. The most prominent example is jQuery, which still remains the most popular JS library in the world. jQuery’s core design is a single über-object with dozens of methods on it, all of which return the same object type so that we can continue chaining. There is even a name for this style of programming: fluent interfaces.
Do they know what's so attractive about method chaining/fluent interfaces? They guarantee returning the same damn object/interface (tho the underlying values mutate). This pipe thing is complete opposite of that, what's the relevance here?
jquery style: a().b().c().d()
However this isn't tree shakable.
modern esm: d(c(b(a())))
this proposal: a() |> b(%) |> c(%) |> d(%)
I've never understood why it's common to dismiss Perl as "line noise" while, outside of contrived or unusual Perl examples, it's way less line-noiseish than what I see in certain languages that are, evidently, very popular among people influential in the JavaScript community.
Implicit, repeated re-assignment of a magical "%" variable while adding a two-character punctuation operator to reverse the "direction" that values "travel" is clean and more readable? In what universe?
The example that also uses Underscore, especially, reads like some kind of parody.
I don't know, why are there so many proposals trying to turn JS into an C#/Java like language? The class syntax, the private fields, TS with its interfaces and abstract classes.
The pipe will lead to more functions and function composition instead of fluent interfaces, which I think is more in line with JS.
> “I was under marketing orders to make it look like Java but not make it too big for its britches. It’s just this sort of silly little brother language, right? The sidekick to Java.”
- Brendan Eich[1]
1: https://thenewstack.io/brendan-eich-on-creating-javascript-i...
Javascript's very OO, deep down in its bones. Its functions are even objects. It was just cursed with an OO model (prototypal) the distinguishing features of which pretty much all fall under "for the love of god, never actually rely on or leverage this functionality in any meaningful way" as far as how good an idea they are for code readability, and with god-awful scoping rules. "Class" and some other changes make the existing OO features usable in a way that won't have your successors cursing you for your "clever" use of prototypes, and saves tons of dumb boilerplate that never should have existed. It also makes it much clearer to the reader when you intend to treat something as an object, and when you're planning to treat it more like a struct.
> "Class" and some other changes make the existing OO features usable in a way that won't have your successors cursing you for your "clever" use of prototypes
That's a strawman, you can write "clever" code with classes that will make your successors curse you just like you can write plain and readable code with prototypes.
It's really far from being ergonomic, unless you're basically just using JS objects as structs. Otherwise you're dancing around its pitfalls and limitations—visibly, in code, not just in your head—to express things that are very simple and clear in pretty much every other commonly-used OO-supporting language.
> I didn't mention OO in general and specifically Java and C#. JS is OO deep down, nobody is denying that, but it's not OO as in Java/C# where writing a function without a class is not idiomatic.
Sure, JS is multi-paradigm, like tons of other languages. Its early design, and early use in the wild, hints at "Imperative with quite a bit of OO and a sprinkling of functional for things like callbacks" being more-or-less its intended style, but of course that can (and has) changed over time, because the core language has half-decent support for all three styles.
Incidentally, I've yet to see a TS codebase that ties itself in knots to avoid defining a function outside a class, to avoid treating functions as first-class when it makes sense to, or even to avoid a little imperative code outside of objects, here and there. They may exist, but I haven't seen it. I have seen a lot of code using HOFs to the point of absurdity, while passing big objects around to a bunch of functions and pretending that's "more pure" than putting those related functions on a class for that object. Or trying to shoehorn runtime-checked immutability into the language, at a performance cost for end users. Or writing their own weird, crippled form of objects in a language that already has them so they can keep things "pure" and let people keep avoiding the Class keyword for... whatever reasons they want to do that (React Hooks).
That might be because I've started OO with JS and mostly did some with it, but in my experience it's fine. I do tend to not use OO as in "object mutating themselves" much though, outside of cases where it's a great fit like a database connection.
> Incidentally, I've yet to see a TS codebase that ties itself in knots to avoid defining a function outside a class, to avoid treating functions as first-class when it makes sense to, or even to avoid a little imperative code outside of objects, here and there.
I haven't seen big codebases like that, but I've had a few collegues coming from Java/C# start every piece of code by writing a class, usually in a "kingdom of names" styles. I'm worried that TS may enable these people even more in the future.
> I have seen a lot of code using HOFs to the point of absurdity, while passing big objects around to a bunch of functions and pretending that's "more pure" than putting those related functions on a class for that object. Or trying to shoehorn runtime-checked immutability into the language, at a performance cost for end users. Or writing their own weird, crippled form of objects in a language that already has them so they can keep things "pure" and let people keep avoiding the Class keyword for... whatever reasons they want to do that (React Hooks).
I'm not defending "functional" (as in ML/Haskell) JS and agree with you here. If people want to do functional programming, there's OCaml, Elm, F#, Scala.js and ReScript, no need to shoehorn that into JS. I think that's an antipattern, just like doing OO everywhere for the sake of it.
JavaScript, in its bones, is a mix between an impure functional programming language like Lisp and a prototype-oriented OOPL like Self; forcing Java-style class-oriented OOP into it is a lot more of an impedance mismatch with its “bones” than FP constructs that are common in the kind of FP languages that influenced it from the outset (though neither is a giant mismatch.)
People want to leave their mark on something big. It's a form of status.
1) consts in JS aren't deeply immutable, so technically these could be mutated. This is a relatively small issue, but also...
2) More and more stuff happens in expression-contexts these days, including (most prominently) JSX. And JS doesn't let you define intermediary consts in expression-contexts (or even in lambdas, unless you build out the full curly-braces and add a return statement). So for many situations the consts version is arduous or even impossible.
2. I am dreading even thinking about how the pipe thing is supposed to operate on JSX. There's the do-expression proposal to achieve what you want in a sane way: https://github.com/tc39/proposal-do-expressions
2. Personally I think it will be quite elegant. JSX itself is an expression, not a function, so it would only ever be the last stage in a pipe (or the context in which a pipe expression is embedded). Note that it would be useful even for just (for example) string transformations that happen to be deeply embedded in some JSX, not exclusively contexts where JSX is directly involved
2b. I didn't know about do-expressions; those look potentially exciting
This doesn't make code less readable, you're just not used to reading code like this.
After a week or two you realize how awesome this is. Piping has been a part of consoles forever now, what's wrong with bringing them into code?
Conversations about more esoteric languages seem to attract the level of discourse HN is otherwise known for.
This is just HN in a nutshell.
I've been wanting to write a blog post for a while about this topic. I have a pet theory that the HN 'feature' of not implementing direct comment reply push notifications (ostensibly a feature to dissuade flame wars) leads people on HN to make very strong statements they often can't defend.
I think it's ultimately a net negative for the community. I get not wanting flame wars, but when you make strong statements on platforms that have the tools for better comment section engagement, you often have to defend your statements. I think this is a good thing.
Doing maintenance of clever FP tricks isn't the same as one liners shell scripts.
It took a while for my brain to get used to it, but once it did I found it so much easier to read, and much more efficient to write.
const [,, { name }] = props;
and: const { $ } = window;
You can kind of reason about what's going on here, especially
if you're a fan of one of the fancy languages with pattern
matching, but if you're just a lowly backend developer in one
of the “boring” languages, and you just need to fix this one
thing in this one JS file, you're not going to have a good time.[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
In the 7 years or so that destructuring assignment has been available, I've never once seen the 'empty destructuring' in your first example. (And if I did, I'd most likely tell someone to just do `const { name } = props[2];`. The second example is a bit silly- if you're using jQuery that much, autoprovide it using a transpiler and be done with it.
My guess is that your org just can't be bothered to care that much about your frontend, so they slap something together that's difficult to maintain, and then blame the language, execution environment, or community for poor results.
function foo({
quux: {
foo: {
bar: {
baz: [first]
}
}
}
}) {
console.log(first);
}
(I've seen things like this), that's what code reviews are for. I find destructuring one of the features that made reading (and writing) JavaScript actually tolerable.But alas, no one ever listens in JS. They just add more and more shit until things are unrecognizable.
People hate plain JS so much, that they’d rather mutilate into the language they prefer, and people are coming from every corner of programming with every type of preference (pun).
One of these days I’m going create a barbarian horde of JavaScript developers and inundate another language, maybe Java, maybe Ruby or Python, and just vehemently insist that they must change their idiomatic style to that of Javascript. Let’s see how they like it.
Anyone know why we have 30 imports at the top of each file like it’s Java? Where did that idea come from? Excessive types, huh? Since when? Have mercy on us, please.
Edit:
These were the welcome and necessary changes IMO:
JS just really needed Classes as syntactic sugar for the odd way we replicated it with prototypes. That and the fat arrow (because binding ‘this’ was cumbersome), along with a basic import statement. Destructuring I can live with. Async/await was also necessary. We really didn’t need shit else.
Small exception:
I’ll make a veery small exception for a minimalist type system, or in other words, only 10% of typescript. I’ll take just the ‘Type’ and that’s all.
Last note:
Coffeescript did a better job synthesizing JS with ideas of other languages compared to ES6 honestly. Sometimes you have to think about how things mix/fit rather than ‘oh this language has it and JS doesn’t, so let’s add it’.
And JS codebases are so much larger than in 2003 that the old idiomatic style is just unproductive.
All we should have are primitive types like object, number, string, boolean, undefined. Anything else should just define a shape/interface of an object, but no "type" that can be inherited.
In that case, as you say, 90% of Typescript could go away and be relatively straight forward.
> JS just really needed Classes as syntactic sugar for the odd way we replicated it with prototypes.
Depends on if that's actually odd. In my opinion, we should avoid prototypes most of the time, so it's strange in retrospect that we layered a cumbersome OO construct on top of it without any of the benefits other languages have like being able to change the constructor/initializer method after class construction.
> We really didn’t need shit else.
Yep. Sadly the older I'll get the more crystalized that realization will become and the more frustrated I'll be at a world that thinks more is better.
I’m advocating for a tighter, more minimalist language. It’s not what you put in, it’s what you leave out, remember?
What parts of TypeScript are you finding would impart more value by having been left out? For example, I find it extremely expressive, and I have gotten huge value out of things like accessing return types of functions, aliasing the non-nullable types in a union type, creating sub-types of massive JSON structures via `subtype = MyType["mySubField"]`, Partial/Pick, Exclude/Omit, ... What things have you seen that you don't understand the usage for?
Final question: what is the most sophisticated UI component you have ever implemented? For example, I regularly implement what are sometimes referred to as "dataflow editors", so maybe this helps explain why I see value in the more niche sides of TS while you come in and decry the construct entirely
Maybe, maybe not. But to entertain you, if the breadth of Typescript solves your niche needs, do you believe most UIs require that same exhaustive usage? Because that’s what’s happening, I’ve seen very simple things overusing it.
I don’t have much more to say about this, but happy to hear your last word.
I worked on a team that happened to be starting on a cross-platform React Native app that needed to have its business logic able to be re-used on a couple other platforms that weren't well-supported by React Native (or React), but did support JS well enough, when we decided to trial TypeScript. The team had done a bunch of "apps" in React and a couple in React Native without TypeScript, before this.
TypeScript made things so much nicer. It cut necessary communication overhead during development down to almost nothing. Turnaround on platforms that were slow to deploy to was a non-issue because things almost always worked on the first try. Navigating the codebase was downright pleasant. Refactoring a library that was used by multiple platforms, a breeze. Simply excellent.
> Do you believe you could not write that UI
No, I wrote these UIs in bare JS and React before, using class-based components and old react state or mobx. I also maintain a library of components because I generally write multiple UIs simultaneously, which of course can use the same building blocks. Since React Hooks & TypeScript, my component library lost about 40% of the LOC (after adding type definitions), looks much prettier, and is infinitely more discoverable with type annotations and autocomplete on everything.
So, do I need it? No, you arguably never need type systems. But the developer experience is an order of magnitude smoother. I option-click into any component when I need to understand its type interface, and I don't need to read any code to do that! I also can seamlessly use graphql-codegen.
But maybe more to the point:
> I’ve seen very simple things overusing it
This is true of every construct, ever, do you not think? People are not inherently good programmers.
Ultimately, I admit that TypeScript and JS have design flaws and non-minimalist features that you can attribute to "dumb evolution" (i.e. people adding features here and there to JS, TS trying to ensure all sorts of JS are covered, TS trying to make sure you can handle all existing types of JS with TS, etc.). However, given the context being that JS & React/JSX are the dominant UI language for web, TS solves this use-case cleanly and with a great level of expressiveness. It is huge and can be overwhelming, but it's important to remember you don't need to use any of these things, and there isn't really. any sort of interop-gulf around TS either (i.e. someone exports a type, you can interact with it, no questions asked).
So, is it graceful from an idealist perspective? No, I concede this. However, it is a tool I would never again turn away from, and while I wouldn't necessarily say it enables me as an individual to do new things, I think it makes it possible to program ideas of high complexity and allow others to work with it much more easily than just JS, which enables me as a team member to boost my teammates.
Shit like:
const [,, { name }] = props;
Is part of why TypeScript's practically necessary in modern JS, if you don't want to go insane and/or let a bunch of stupid bugs sneak in.For example, types vs interfaces in typescript - they can be used interchangeably in many cases, and not in a few other cases. [1]
Let's KISS, not over complicate for the sake of more features that may be negligible in value. And let us especially not introduce features that can overlap.
[1] Example: "A class can implement an interface or type alias, both in the same exact way." - https://stackoverflow.com/questions/37233735/interfaces-vs-t...
C++'s syntax problem, to my mind, is not that it's excessively large, but that it's excessively clever and error-prone, and sometimes not very uniform. It's hard for humans to parse.
This proposal does not introduce any special cases, it adds one (or two, if |?> is also accepted) very clearly defined operators, proven by practice. It makes parsing things by humans easier.
> To emphasize, it is likely that an attempt to switch from Hack pipes back to F# pipes will result in TC39 never agreeing to any pipes at all. PFA syntax is similarly facing an uphill battle in TC39 (see HISTORY.md).
While I can respect differences in opinion, this feels heavy handed.
Once a feature gets in, it will never get out. Whereas until it gets in, we can still hope for F# pipes :-)
However, I feel like all the people using Ramda should just make their own language if they don't like the direction JS is going in. This is a "too many cooks spoil the broth" situation.
Then began the sheedding.
Why does pipes haveto deal with async? Async is 99.9% IO realated, and now it has to be shoehorned into something thats by nature sync.
On top of that now theres a magic variable so you could pipe functions accepting multiple params.
He beauty of a pipeline is that its naive, and forces a certain code style, makin testing more easy, and code more elegant.
Today im on the fence, as im not liking what the proposal has become.
The spec points to the `%` placeholder-y thing from Hack which shows how weird piping gets in an uncurried language like JavaScript. I think these issues illustrate how nontrivial adding piping would actually be in JavaScript.
Currying is the obvious, simplest solution because it's always one parameter in and only one thing is returned (often a function awaiting one new argument). But this would require massive fundamental changes to the builtins probably requiring something like "use currying" to tell the user agent to flip the argument order and curry all the functions. I'd be pretty into this though.
Could you just clone them and expose versions with swapped arguments under a different namespace?
Given
f :: a -> b
g :: (b, c) -> d
h = uncurry ((curry g) . f)
Then h :: (a, c) -> dFor currying, for instance, you need to know exactly when to execute the function and return the results, vs. when to return another curried function expecting more results. This is messy in the face of optional arguments, and in Javascript, all arguments are always optional.
"But I can solve that with more syntax!"
Perhaps so. But then you have the problem, what if you want to take advantage of optional arguments to add another argument to a widely used function? Do some of your invocations of the function that previously considered the function "done" now return functions expecting one more argument? Probably, unless your syntax forced explicit specifications of when to curry. Dynamic languages kinda have that sort of problem all over the place anyhow, but this would be a new manifestation for people to deal with. That's getting ugly and complicated.
You're probably better off just using the lambdas the language already has rather than blundering your way through this mindfield. I don't think I can name a single case of a language solving this problem after the fact; by all means upgrade my knowledge by naming one that did (no sarcasm, I'd be interested in seeing it).
This upgraded my knowledge, thank you. Also I love that phrase, thanks for that as well!
Which, I suppose, is out of question. It's hard to introduce in a backwards-compatible manner.
Why, I won't mind if MS developed a variant of Elm instead of TS. But that would be not realistic for a number of reasons. You can only turn a supertanker slowly.
Laughs in LISP.
Used to?
(-> "a b c d"
.toUpperCase
(.replace "A" "X")
(.split " ")
first)
"X"F# style and a separate partial application proposal that could work anywhere in js (instead of just in a pipe) seems preferable to me.
I'll throw my hat in with those on the sidelines excited for the language we're stuck writing more of than we may want to, getting another piece of syntax that improves the ergonomics in a lot of little cases. Much like destructuring, much like the optional chaining and null coalescing. It will be nice to avoid the occasional temp variable that isn't useful for readability, or to rearrange expressions that read better applied than composed... but mostly I'm looking forward to how much nicer a pipe operator makes iteratively doing something in the console.
https://docs.microsoft.com/en-us/dotnet/csharp/programming-g...
It's a bit sad that the discussion about the two different proposals slowed things down so much, but it's understandable.
I'm looking forward to pipes!
As far as decorators as a "need". You're right. It's often as easy as `functionName(...args)(target)`. The decorators are definitely convenient for those coming from other languages that have annotations though.
Object.keys(envars)
.map(envar => `${envar}=${envars[envar]}`)
.join(' ')
|> `$ ${%}`
|> chalk.dim(%, 'node', args.join(' '))
|> console.log(%);
Okay, from the other comments, I gather this is like Hack. Does any other language use a pipe operator for non-unary functions? Does the `%` syntax apply more generally? What is the type of `foo(%, 1, 2)`? One would expect it to be the same as `placeholder => foo(placeholder, 1, 2)`, but is it?For example, in Scala, if I have `foo(Int, Int, Int): Int`, then `foo(_, 1, 2)` has type `Int => Int`. That's true anywhere, but in the JavaScript proposal we seem to have a syntax that only applies in a special case.
The older I get, the more I care about the latter.
This is the first ES6+ proposal that I'm not excited about & I don't know how to voice that concern because it is so subjective.
How about the null coalescing operator that literally has zero use outside of encouraging an anti pattern in failing to disambiguate null and undefined at the earliest possible opportunity?
How the fuck did that ever make it into the language?
Some of the upcoming proposals are absolutely ridiculous too. A lot of these are ideas that would be fine for niche use cases, but the proposal itself has examples so stupid that they should be shot down immediately because the person doing the proposal doesn't know what the fuck they're doing (e.g. the ridiculous jsx example in the 'do' operator proposal where for some reason we now want to shoehorn a block of procedural code into a 50 line declarative expression instead of just using a basic one line ternary operator that has been in the language for years).
What's up with that implicit % argument??
Why are the authors so opposed to temporary variables?
Having more explicit and correctly named variables makes the code so much easier to read vs slapping a % in the middle of the code.
I would love to have pipes, but this is kinda ugly to me.
But imperative languages are really good sequencing! The syntax for doing this in Javascript is ;
Maybe pipes will give you some syntax wins, but it's not a huge win the way it is in declarative languages. I feel like what we are seeing in general is a lot of the good ideas of declarative languages making their way into imperative languages, sometimes without the best rationale. I'm not really seeing anything compelling in this proposal. It focuses a lot on legibility, but is that the biggest win here?
And is it an unambiguous win? If you're going to go through all the trouble of adding this thing, it better be fore a really good reason with clear benefits that outweigh the costs. I worry that with some of the examples they give (probably the best cherry-picked ones they could find), I find the temporary vars version more understandable. Yeah there are fewer keystrokes, but I have not the slightest idea what this does:
|> chalk.dim(%, 'node', args.join(' '))
But I know exactly what this does: const coloredConsoleText = chalk.dim(consoleText, 'node', args.join(' '));
Also, another reason this is not so great in Javascript is because the functions are impure. In pure functional languages you can chain functions together safely and with certainty because you know:1) input arguments are immutable and will not be modified by the function
2) The function has no side effects.
Therefore you can run these pipelines multiple times with the same input and you know what output you're going to get.
If you have one or neither of these guarantees, then it's the wild west. What happens to the arguments when they go into that function? Are they mutated? Are they shared with another thread/process/worker? Are there any system calls in there? File IO? HTTP Requests?
With this proposal, you have all these side channels that can be used and no guarantees about mutability or idempotency, or even whether or not the variables will have the same type after they are passed into a function. This just seems like the makings of a huge mess to me.
(-> "a b c d"
s/upper-case
(s/replace "A" "X")
(s/split " ")
first)
But I guess in JS you wouldn't know when to stop the piping.Also, in Clojure, you have -<> which is like a hybrid between Hack and F# pipes. Basically it defaults to piping into the first argument, but if you use <> anywhere, then where you've used it is where it will be piped instead, best of both worlds.
(-<> "a b c"
s/upper-case
(str "result:" <>))The standard library has as-> , which doesn't have the implcit default. So you'd write
(as-> "a b c" x
(s/upper-case x)
(str "result:" x))Brendan Eich managed to get a lot of things right for such a tight scope, along with some very unfortunate decisions, of course.
Now we have a language that just keeps on growing, the old quirkiness can't be swept away, and new ones keep adding up.
I'm not sure it's a win.
The target of JS wasn't pro developers in those days anyway, so familiarity wasn't really a consideration.
Most importantly, all the "new" features of JS would have existed for 25 years now. JS would have been fast in 1995 instead of waiting until 2008 to be usable. We've take years to add simple things like integers or arrays to JS that would have just been there. We're still debating things like threads which should also have been a solved problem by now. If you didn't like scheme, compiling to the language from another would be trivial too.
Sure, that doesn't hurt.
>The target of JS wasn't pro developers in those days anyway, so familiarity wasn't really a consideration
Lisp like syntax is very peculiar. It puts off novices and pros alike.
I didn't really understand you last paragraph other than agreeing that integers should have been there from day one. Array-like objects are not so bad.
It would be more proper to say that it has no syntax. It's like everything is just a function call.
`add(1, 2, 3, max(4, 5))` vs `(+ 1 2 3 (max 4 5))`
or
`_.reduce(list, function (acc, num) { return max(acc, num); })` vs `(reduce list (fn (acc, num) (max acc num)))`
Very few devs who code in lisp for 2-3 months come away hating the syntax. Hating something strictly because it is different is not a great life perspective IMO.
> I didn't really understand you last paragraph other than agreeing that integers should have been there from day one. Array-like objects are not so bad.
Scheme has a lot more than integers.
* Rational numbers, Complex numbers, integers * vectors (real arrays) * Linked Lists * Proper Tail Calls * Lazy evaluation (the basis of things like generators, iterators, and the event loop) * No duck typing * No weird evaluation rules * Fast implementations. The fastest like Chez Scheme JIT are very fast compared to v8. * Macros for new features
Then there are all the common SRFIs standards that pretty much every big-name scheme implements.
* Typed vectors (specify primitive array type) * Multi-dimensional arrays * Hash Tables/Maps/Sets * Multiple value returns * Records (structs) * Objects * Extensive String functions (way more than JS currently offers) * Multi-threading * Streams * And at least 200 more features
Think about ES6+. The features fit into two categories. The first is primitives (TypedArray, Set/Map, const, etc). The second is syntax (class syntax, destructuring, generators, arrow functions, exponent operator, etc).
Those "new" JS primitives have existed in Scheme since the 70s and would have existed in the browser for 25 years now instead of a handful of years. The second class range from unnecessary to already present in macros to easily implemented in macros. There would be no need for transpilers; we could have grown the language without needing all those backward-incompatible language releases.
CSS would be different. The DSSSL proposal (literally scheme) would have replaced the current nasty C-like one.
Likewise, we wouldn't have suffered through XML waiting for JSON to arrive. JSON's syntax is just another expression of lisp lists. `{foo: [1 2 3], bar: "abc"}` would have been immediately obvious as `((foo . (1 2 3)) (bar . "abc"))`. There wouldn't be an extra JSON parser because this is built into the language. XSLT is just very bad lisp macros, so even that excuse would not exist.
Given time, we might have even seen lisp-style lists replace HTML with `<div class="foo">123</div>` becoming `(:div ((class . "foo")) "123")`. If you think about it, this is just `React.createElement("div", {class: "foo"}, "123)`. As this is also dead-obvious, we would have had much better JS frameworks decades earlier and without nearly as much framework churn.
Wasm also wouldn't really be a thing either as Scheme would likely be fast enough and a very reasonable compile target. Type hints like what Common Lisp uses are already very fast (around Java performance and that's with just a couple (amazing) hobbyists rather than a huge, paid dev team). I would note though that wasm uses s-expressions in non-binary form.
This isn't just theoretical. When Paul Graham said Common Lisp was their secret weapon in the 90s, this is what he was talking about. Today, using Common Lisp, you use CL-WHO to turn those s-expressions into HTML. You use parenscript to turn S-expressions into JS. You also use something not too unlike DSSSL to turn s-expressions into CSS. It's lisp all the way down to SBCL which compiles s-expr assembly into binary for the computer to execute.
There are even more results. Initially, Steve Jobs only wanted web apps on the iPhone. It turned out that JS just wasn't fast enough at the time. If Scheme had been the language, the web would have most likely been fast enough for almost everything. The current stagnating Mobile Safari and app store lockdowns would probably have never happened.
Scheme instead of JS would have dramatically altered computing as we know it and I think it would have been for the better.
I agree that Lisp’s “no syntax” is an acquired taste, but asking someone to keep an open mind and use something that peculiar for ≈3 months is a very tall order.
Most people look at the curtain of parentheses and reject it at the spot.
I’d love to have had Scheme on the browser, but I’m not sure it would have succeeded.
This example:
// Status quo
return filter(obj, negate(cb(predicate)), context);
// With pipes
return cb(predicate) |> _.negate(%) |> _.filter(obj, %, context);
Should in my opinion be written as: let result = cb(predicate)
result = _.negate(result)
result = _.filter(obj, result, context)
return result
The main benefit of this is its experience and almost language agnostic. You see a variable defined and it's run through a series of functions before being returned. What's the point of removing a few characters?edits: figure out how to format code, don't omit things
return cb(predicate) |> negate |> filter;edit: wasn't to isn't
Notably, on the first point, you proposed version drops two input variables from the other two versions, and on the second, the pipes version would correspond to the “status quo” version if the “_." prefixes were dropped.
It looks like you've made your proposed version look cleaner by both omitting required elements from it and adding unnecessary (and code-breaking) elements to the with-pipes version.
I actually just recently saw it in production code for the first time and was like "whaaaat is that?" It had been so long since I saw one that I didn't recognize it at first.
The thing is, usually in real haskell code you aren't going to be using something that is roughly like set builder notation. Well at least, I haven't seen many scenarios where its needed. You're pulling data from someplace, manipulating it, etc, and then sending it somewhere else.
That's not to say that I'm absolutely correct on this or whatever. But to me, overall I think Haskell syntax is far too complex and has too many special cases, and given list comprehensions are exactly equivalent to doing the same thing via the list monad, I'd prefer they be gone.
I fear that someday we'll collectively need to replace GHC, and each and every weird edge case just means that its that much more of a giant task when that day comes.
Like this?
stream = (manipulate(item) for item in producer_stream()
if passes_filters(item))
consumer(stream) fmap manipulate $ filter passes_filters producer_stream
And GHC has list fusion rules in this case (not sure if they exactly apply for list comprehensions; they probably do, but i'm not certain of that.)so IIRC the xyntax in haskell syntax your example would be smth like:
[ manipulate x | passes_filters x, x <- producer_stream ]
which is transformed to: do
x <- producer_stream
guard (passes_filters x)
manipulate x
Which I believe does not have the same run-time performance as my fmap/filter example; even if it does, I'm just saying I don't see it often, and its not idiomatic.Its different in python though because, well, python is more cursed.
However, I prefer the comprehension and `do` version over your `fmap` expression. The reason is that I think that filtering and transforming a stream is sufficiently common/important that a language should provide declarative syntax / a DSL for it. Whereas your `fmap` version makes too many implementation details explicit: all that should be needed is for the programmer to say "lazily filter and transform these values". I think it would be ideal for the programmer not to have to worry about `fmap` and `$`, and I think the expression should read more naturally than your fmap example.
The Python and Haskell comprehension versions pass the "read naturally" test very well: "create a set/stream of manipulated items where the items pass the filter".
The Haskell `fmap` version reads as "apply the manipulate function to a functor where, oh by the way the functor is a kind of list, and oh by the way use $ to separate arguments in this expression, but as I was saying do that but filter the list first".
EDIT: I don't mean to imply that I know how Haskell should be written, I defer to you; the sophistication of Haskell's functor-related abstractions is such that I bet you're right that the special case syntax of a comprehension is kind of an annoying presence in the language (but that still leaves two ways to do it?). But, we were talking about javascript and there I think it makes a lot of sense to give the world a way to construct collections declaratively.
I tend to vastly prefer a small, beautiful, principled, "growable" language over one that has many builtin forms. e.g. Scheme. Because, as I said, each of these features just makes it harder for other implementations.
FWIW I also don't think it made sense for JS to get a pipeline operator; only reaosn I like those is when a language supports user-defined operators. But, anyway.
`foo(...)` normally means "call `foo`, but not with pipes
function foo(str) {
return `wrap(${foo})`
}
function bar(prefix) {
return function(str) {
return prefix + str;
}
}
'hello' |> foo;
'hello' |> foo($);
'hello' |> bar;
'hello' |> bar($);
'hello' |> bar('myprefix');
'hello' |> bar('myprefix')($);
What of those expression does the right thing?It would be nice to see more functional features, similar to async/await, template literals and lambdas. Some things that I think would be interesting are optional type hinting (native), exhaustive pattern matching.