Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Moving ahead with the minimal proposal #167

Closed
pygy opened this issue Apr 25, 2020 · 267 comments
Closed

Moving ahead with the minimal proposal #167

pygy opened this issue Apr 25, 2020 · 267 comments

Comments

@pygy
Copy link
Contributor

pygy commented Apr 25, 2020

In Rust land, it is a common practice to implement features in stages, with the initial steps designed to be forwards compatible with possible future evolutions that have yet to be settled. It works for them with great success.

I think that the minimal proposal should be shipped, after being amended to make sure it is forward compatible with both the smart or the F# proposal.

In #163 (comment), @ljharb said:

Establishing that ability to augment it later would help a lot - but there’s still folks who don’t think either one is worth the syntax cost - they have to be convinced, and only meeting a subset of the use cases by intentionally shipping a subset of the semantics doesn’t seem to me like it’d be compelling enough to convince them.

Folks: this proposal is not for you. Not every proposal has to be useful to every programmer.

Classes come to mind as the seminal example. Taxonomies are easy on the mind, but a very poor way to model the world at large. Classes are thus a nice way to model some phenomena, most of which are human-made, but they often are not the correct tool to model a problem even if you try hard to shoehorn it. We could do that kind of modeling with JS using prototypes, but taxonomies were deemed sufficiently important to need their own sugar.

In my code, I treat the class keyword as a syntax error... but I also understand that OO modeling is central to how some people think and write programs and I'm fine with it!

The same applies here. This is badly wanted by a subset of the users, who would probably rather have this than infix arithmetic operators. Please have some empathy.

@ljharb
Copy link
Member

ljharb commented Apr 25, 2020

I think that it's more accurate (altho not entirely, ofc) that every proposal is for everybody, because everybody may see it in the future and thus be forced to learn it.

Empathy includes understanding that syntax has a cost, and that even though you, personally may consider it worth it, that doesn't mean every current and future user of the language will agree. That's the burden on those adding new things to the language, and that's why "no change" is very often much better than "the wrong change".

@pygy
Copy link
Contributor Author

pygy commented Apr 25, 2020

So I guess I should count you among "those who feel it is too much". Good. I prefer to have a concrete interlocutor!

The price here is minimal: it's a pipe, the data flows through it.

Compare that to the mountains of complexity that classes and decorators add, and that is deemed essential.

People may have to see the pipeline but they won't have to use it (well, unless their boss tells them so, eh).

With Web Components, the WhatWG imposes the complexity of classes to everyone and their daughter.

Then this little operator adds too much complexity? It feels like a double standard.

@ljharb
Copy link
Member

ljharb commented Apr 25, 2020

You certainly can't; I love the pipeline operator and dearly want it.

class simplifies the complexity that already existed in the language. Decorators' stage 2 implementation was blocked by implementors, and it's not clear if it will arrive in the language at this point nor what that solution would look like.

Just like everything else, people will have to use it if they maintain code that contains it, or if they're debugging code they depend on, that uses it.

@pygy
Copy link
Contributor Author

pygy commented Apr 25, 2020

Good to know :-). Back to some abstract foil then...

Point taken re. maintenance, and I'm ususally a proponent of small languages, but this, and especially the minimal proposal adds minimal complexity, either syntactical or conceptual. There are no traps like the dynamic scope of this and the headaches it can impart to newbies.

The exact semantics of |> await could be less obvious actually. But the synchronous pipeline that prohibits await and mandates parens in every scenarios involving arrow functions is very simple to understand, and the complexity doesn't compound with other language features.

The syntax could be confusing for a total beginner, but then, the first Google result for |> is already MDN... (DuckDuckGo isn't as good).

FWIW, debugging a pipeline is trivial:

const log = (...args) => (console.log(...args), args[0])

const x = foo()
  |> log
  |> barzouille
  |> log.bind(null, "post-barzouille: ")
  |> bazribouille
  |> log

@aadamsx
Copy link

aadamsx commented Apr 26, 2020

I think that it's more accurate (altho not entirely, ofc) that every proposal is for everybody, because everybody may see it in the future and thus be forced to learn it.

I NEVER use the class keyword in JavaScript. The class proposal wasn't for me (IMO it was a waste of time and effort), and I think a lot of people are in my boat. Using your reasoning, TC39 should never have agreed to implement it.

Empathy includes understanding that syntax has a cost, and that even though you, personally may consider it worth it, that doesn't mean every current and future user of the language will agree. That's the burden on those adding new things to the language, and that's why "no change" is very often much better than "the wrong change".

Almost the same point, I'm a current and future user of JS and I don't think adding the class keyword was useful. By your reasoning, since "lots" of people don't use or need to use class, like myself, it was "the wrong change". I don't think your reasoning holds up very well here.

.

IMO these arguments are red hearings. This is a light, easy to understand syntax addition that will bring joy to countless JS current and future users.

.

From reading the threads on this repo, we seem to have a few "committee" members that for one reason or another are against it, for no reason that's clear to see, and will hold this up indefinitely from what I can tell.

I just wish we could somehow get the "committee" members on record and find out exactly what the concrete objections are. We NEED better dialog from them, but it seems this isn't going to happen with the current TC39 leadership/custodians if past is prolonged.

It's been a disappointment (at least to FP developers like myself) to have such a important proposal sitting in TC39-limbo for years.

.

OO devs got their class keyword and related sugar, why can't FP devs get our |> and partial application, come on TC39!

@ljharb
Copy link
Member

ljharb commented Apr 26, 2020

@aadamsx except that the objects created by the class keyword haven't changed. If you had to maintain any code using inheritance, that code is ALWAYS easier to maintain when using class then when not.

The thing we need to convince people of is that "code using the pipeline operator" is always easier to maintain than code not using it. Construct that argument, and we can get it! Merely complaining that things are going too slow isn't going to help anyone.

@pygy
Copy link
Contributor Author

pygy commented Apr 26, 2020

that code is ALWAYS easier to maintain when using class then when not.

Provided it is first written/ported to USE the class syntax. But provided taxonomy is often not the proper solution, it just makes what I see as a bad pattern easier to fall into. Writing/porting that code to use better patterns would make it even easier to maintain.

The elephant in the room in this case is that the way the DOM is implemented, and the fact that inheritance was seen as the natural way to extend it. I see that as a lack of imagination, but that ship has once again long sailed.

Also, "adult" languages in vogue at the time had classes, JS was seen as a toy, it needed classes to look like a grownup.

Complaining that things are going too slow may not help, but framing this in terms of empathy does.

https://twitter.com/awbjs/status/1085557705371078657

@JAForbes is brilliant. He's well aware of the the cost of using arrays here. Yet he's yearning for some kind of syntax for pipes, and ends up being mocked, schoolyard-style, by senior TC-39 members.

[x]
    .filter(Number.isFinite)
    .map( x => x.toFixed(2))
    .concat('N/A')
    .shift()

This is the kind of patterns FP folks resort to in native JS to compensate for the lack of piplines. If that doesn't look like pain I don't know what does.

If you can't tell red and green apart, no matter of explaining will help. My point is that there are people on the TC who are colorblind, and in denial about it. Not everyone sees the world through the same lens.

@anatoliyarkhipov
Copy link

anatoliyarkhipov commented Apr 26, 2020

If you had to maintain any code using inheritance, that code is ALWAYS easier to maintain when using class then when not.

If you had to maintain any code composing functions, that code is ALWAYS easier to maintain when using |> then when not.

The thing we need to convince people of is that "code using the pipeline operator" is always easier to maintain than code not using it.

In that case you have to construct an argument that "code using the class keyword" is always easier to maintain, but for some reason you constructed an argument for "code using inheritance", not class keyword.

@noppa
Copy link
Contributor

noppa commented Apr 26, 2020

@pygy For what it's worth, pipelines wouldn't actually help that example.
But I see your point, I have also seen [x].map used as a "poor man's pipeline".

@pygy
Copy link
Contributor Author

pygy commented Apr 26, 2020

@noppa In that code, x is an arbitrary expression.

x |> (x => ternary) would be the correct pattern. James' Twitter audience is mostly FP programmers, and his sample implied a stream, if you squint hard enough. Code like that exists in code bases today, and would be better written with the pipe.

I'm not sure why TC-39 members jumped on James like that, there may some context that's been lost to me. The pile on looks bad though, and is IMO representative of the lack of perspective and empathy.

@beem812
Copy link

beem812 commented Apr 30, 2020

How did we go from "which way do we want to do this obviously useful feature that's more valuable to general programming in javascript than any other proposal up for discussion?" to "Do we want this at all?" It's hard to join this discussion and not be inflammatory because there are no people who have coded in a language with this feature that would come back and say no we don't need it here in javascript land.

@littledan
Copy link
Member

Our style in TC39 tends to be a bit more conservative, where we try to hold off until a feature is "done".

I think it'd be worth it to move ahead with something in the spectrum from minimal to F#, if we can agree that they're self-contained enough. I could understand the argument that they're not complete enough to ship until we have partial application, though (which is more complicated and I'm not sure will be acceptable to the committee).

@szTheory
Copy link

szTheory commented May 1, 2020

With the trend of CPUs increasing their number of cores, making functional programming easy will set JavaScript up for success in concurrent programming since you can avoid a lot of state issues. The pipeline operator will be great for this. It's intuitive and has a proven track record in other languages.

@littledan
Copy link
Member

@szTheory This proposal is much more superficial than something that would enable that...

@szTheory
Copy link

szTheory commented May 1, 2020

If functional programming makes concurrency easier, and the pipeline goes a long way toward making functional programming easier, it seems like a win. Having an ergonomic way to compose functions is a real game changer.

@littledan
Copy link
Member

I think immutable datastructures are pretty separate from methods vs functions. They're both referred to as "functional programming" but the composition of data and code are just different things.

@hlehmann
Copy link

hlehmann commented May 10, 2020

Based on what exists today:

# Rxjs
const operator = pipe(
  (obs) => custom(param)(obs),
  map(x => x+1),
  filter(x => x % 2 === 0),  
  custom2(param),
)
const obs2 = obs1.pipe(operator)

# ramda
const operator = R.pipe(
  R.multiply(2),
  (x) => x + 1, 
  Math.abs, 
)
operator(-4) //=> 7

# lodash-fp
const operator = _.flow([
  _.multiply(2),
  (x) => x + 1, 
  Math.abs, 
])

Those pipelines work the same and have been tested and used by the community for quite some time.

What are the limitation for having |> working the same way ? |> having similar priority than ,

const y = x
  |> R.multiply(2)
  |> (x) => x + 1
  |> Math.abs

const operator = (x) => x
  |> R.multiply(2)
  |> (x) => x + 1
  |> Math.abs

eventualy but clearly not priority, something like

const operator =
  |> R.multiply(2)
  |> (x) => x + 1
  |> Math.abs

@Jopie64
Copy link

Jopie64 commented May 10, 2020

How would the last part work? Currently the proposal states that |> is immediately evaluated like in f#. When you don't want that you need operator >> in f#.
Which made me think: how about instead of adding a new operator to the language, couldn't we add a way to create an infix operator like in f#, so even operator |> can be declared in userland, something like this:

const (|>) = (left, right) => right(left)

Or >> like

const (>>) = (left, right) => arg => right(left(arg))

Or is that going too far, or impossible in JavaScript? 😊

@andykais
Copy link

@hlehmann Probably worth noting that at least in the case of rxjs, creating an operator from the pipe function delays actually creating the pipe until an observable is given to set as the source. https://github.com/ReactiveX/rxjs/blob/master/src/internal/util/pipe.ts#L33. So given the current available syntax, it is implementation-wise the same as:

import * as Rx from 'rxjs'
import * as ops from 'rxjs/operators'

const square = (x: number) => Math.pow(x, 2)
const add = (x: number) => (y: number) => x + y

const operators = (observable: Rx.Observable<number>) => observable
  |> ops.map(square)
  |> ops.map(add(1))

operators(Rx.of(4)).subscribe(console.log) // prints 17

@xixixao
Copy link

xixixao commented May 21, 2020

Folks I agree that we should move forward with a minimal proposal. I feel like it's hard to jump in here without starting another long back and forth, but I strongly believe the best minimal proposal is Hack style only.

Indulge me to quickly lay out why:

  1. I have been writing Hack and modern JS for the past 5 years full time, and trust me that these languages are rapidly converging to the same style of programming. What works for one language, generally works pretty well for the other. The Hack style has been chosen for Hack and it's working great in our codebase. F# or Elixir on the other hand are languages with much, much more different feature set and style.
  2. The Hack style only proposal is simpler than the smart proposal. I think this is not controversial. It doesn't break referential transparency (smart proposal: foo = baz(); x |> foo works, but x |> baz() errors). It doesn't require special handling of methods. It is SIMPLER, MINIMAL.
  3. Its only real downside is that you have to write 3 more characters for simple function application. x |> foo(#) instead of x |> foo. So first we should ask: How often does this happen? I've checked a couple of our product codebases and in Hack it's very regular: 40% of the time. 40% of the time we write something like $x |> Namespace\function($$) or $x |> Class::method($$) or $x |> function($$) - 60% of the time the RHS expression is more complicated than that. Does it suck? No, it's just what it is. The fact that the syntax is consistent between these two groups is much more important than saving 3/4 characters.
  4. The smart and F# proposal introduce a completely foreign concept: Function call through novel syntax. Today, there's two ways to call a function, either you call it directly: foo(), or you pass it to a function that calls it: baz(foo). There is no place in JS where you would stick a function somwhere and it would get magically called. We do await foo() not await foo. The Hack style preserves this. The other proposals add this foreign concept to JS: For this one special case, you have to remember, that the syntax is actually gonna call the function under the hood.
  5. Point-free style is hard to read. We have many codebases at FB that use it and are essentially in-accessible to people outside of their maintainer teams because of it. We shouldn't push JS in that direction, it's a step back. edit: More explanation on this point

There's nothing you cannot do with the Hack-style proposal that you can do with the other proposals if you're just willing to type 3 more characters.

Wouldn't you all agree that having Hack-style available in Chrome today would beat the "niceness" of not having to write the 3 characters? Let's get going with a minimal proposal. If it really kills people we can add the "smart" syntax later, but if we start with F#/smart we can never go back to plain Hack-style.

@tabatkins
Copy link
Collaborator

tabatkins commented May 21, 2020

@xixixao That is an excellent summary of many of the reasons I so strongly favor topic-style (hack-style). And in private discussion with the other pipeline champions while we prepare for the June meeting, I've also come to the same conclusion regarding "smart mix" - the three saved characters isn't worth the break in referential transparency or overall consistency. As such, I'm going to be arguing for plain Hack-style at the upcoming meeting.

I think my only two remaining arguments are:

  1. Due to the requirement that the pipeline body is a function, F#-style is fundamentally incapable of handling await or yield without a special syntax form, because the author needs to put a function wrapper between them and the outer function. Anything we introduce in the future that is also aware of function wrappers will similarly require special handling in the pipeline, or else won't be usable there at all.

    Hack-style automatically handles await, yield, and all future similar syntax constructs with zero novel syntax. Given how important asynchrony is today, and how I expect it will continue to become more important in the future, this is a big deal.

  2. If you want your function to be usable in a natural-feeling way with F#-style, you have to write it in a style that is not remotely common in JS, where the most important argument is omitted from the arglist and is instead received by returning an unary function taking that argument. Not only is this weird to write, but virtually no functions in existence today are written in that style, so almost no pre-existing functions can be called in a natural-feeling way with F#-style. (edited to add: unless they're unary to start with, then they're fine.) And finally, if you do write a function to be natural in an F#-style pipeline, it's then unnatural to call it manually! Code like foo(1,2)(3) is virtually unheard of in today's JS, for good reason.

    Hack-style, on the other hand, accepts all functions, in whatever style, and they're exactly as easy and natural to use in the pipeline as they are outside the pipeline.

@Jopie64
Copy link

Jopie64 commented May 22, 2020

One of the big use-cases of the |> operator is RxJS operators. In F#-style, instead of the userland pipe function, you could write it like so:

const queryResult$ = searchQueryInput$
  |> debounceTime(300)
  |> switchMap(query => http.get(url, {query}))
  |> map(formatQueryResult);

Is there a nice way to write something like this when Hack-style was used?
Note that RxJS is just one of the use cases where the argument that changes the most (often the 'receiver') is curried and put at the end. (Think of ramda, where this style is used a lot.)

@anatoliyarkhipov
Copy link

anatoliyarkhipov commented May 22, 2020

@tabatkins can you please elaborate a bit on the point 7?

Especially on what you meant by this phrase:

virtually no functions in existence today are written in that style

And why you think it's required to sacrifice ergonomics of manual call to write functions in that style? Ramda clearly shows us it's not necessary:

R.map(x => x*x, [1, 2, 3]) 
(3) [1, 4, 9]
R.map(x => x*x)([1, 2, 3]) 
(3) [1, 4, 9]

@matthewwithanm
Copy link

@Jopie64 I think the argument is that these are outliers in the JS landscape. Sure, you could write |> debounceTime(300)(#), but it seems more likely that, were hack style to become prevalent, we'd favor designs that used plain-old-non-curried-most-important-arg-first functions and do |> debounceTime(#, 300) instead.

@anatoliyarkhipov
Copy link

anatoliyarkhipov commented May 22, 2020

@matthewwithanm how you determine who are outliers? Is there some statistics on which style is preferred, say, by people often using pipe functions?

@matthewwithanm
Copy link

@matthewwithanm how you determine who are outliers? Is there some statistics on which style is preferred, say, by people often using pipe functions?

I'm not arguing for one style or the other but it doesn't seem controversial to say that the curried style isn't the dominant one in JS today. (I'm not talking about the preference of a subgroup, just what's most common.) Of course, you might weight the importance of that differently than others do.

@ducaale
Copy link

ducaale commented May 22, 2020

were hack style to become prevalent, we'd favor designs that used plain-old-non-curried-most-important-arg-first functions and do |> debounceTime(#, 300) instead.

In the F# version of this proposal, the Partial function application proposal serves to bridge this gap so I wouldn't be concerned about the community turning every function into curried unary functions.

My main concern with the hack version of this proposal is that it can potentially block the partial function application proposal from moving ahead. Not only that proposal would be used in conjunction with the pipeline operator, but it is a general feature that can be used in other parts of the language compared to the hack topic which is only usable inside the pipeline operator.

For example in Reactjs, it is normal to pass partially applied functions as a prop to other components. The partial function application proposal would make that easier. Would the hack topic be able to fill that role?

I believe that Javascript evolution as a language should be through small proposals that work together, that generalize, and which are consistent, Not via a big proposal that tries to solve everything but fails to do so.

@ljharb
Copy link
Member

ljharb commented May 22, 2020

In what way you do see the hack-style as obstructing partial application?

@ducaale
Copy link

ducaale commented May 22, 2020

tc39/proposal-partial-application#36 (comment)

The biggest blocker to partial application has been proposals such as the smart pipelines proposal, which is attempting to do something similar with wildly different syntax. This proposal initially was intended to dovetail into the (F#-style) pipelines proposal as a way to resolve the first-arg (lodash-style) vs. last-arg (Ramda-style) debate.

I've essentially put this proposal on hold until the pipeline proposal has settled on a direction.

@mAAdhaTTah
Copy link
Collaborator

Long-term, we'd have to build partial application on top of Smart Pipelines, should we go that way, into a more generalizable form. The proposal repo has extensions for this; specifically, pipeline functions.


This doesn't make sense to me:

The smart and F# proposal introduce a completely foreign concept: Function call through novel syntax. Today, there's two ways to call a function, either you call it directly: foo(), or you pass it to a function that calls it: baz(foo). There is no place in JS where you would stick a function somwhere and it would get magically called. We do await foo() not await foo. The Hack style preserves this. The other proposals add this foreign concept to JS: For this one special case, you have to remember, that the syntax is actually gonna call the function under the hood.

How is x|> func "novel syntax" compared to x.map(func)? How is func not "magically called" in the second but is in the first?

@lozandier
Copy link

lozandier commented Apr 29, 2021

@highmountaintea How does the partial partial application proposal or its inclusion in the F#-style proposal not alleviate the concern you shared regarding "currying ahead of time"?

It does. I was simply saying syntactically there is a difference between unary functions and in-place currying functions. Using unary functions in pipeline examples is not a good representation of real world usage.

I respectfully disagree; it's even common in POSIX-compliant process buffering / terminals that accommodates anonymous pipelining:

A classic example:

ls -l | grep key | less

In general command1 | command2 | command3 is a pattern developers have been familiar with for years data piplining & again FP libraries among the most popular libraries in the JS ecosystem have functions to enable this style challenges your idea that it's "not a good representation of real world usage"; data transformation functions natively in JS follow this pattern such as Array.map, Array.reduce, Array.filter & so on. Mixins follow this pattern as well, it's just messy without the pipeline operator proposed by this spec:

class MyElement extends someOtherMixin(connect(store)(HTMLElement)) {
}

Becomes

class MyElement extends HTMLElement |>  connect(store) |> someOtherMixin {
}

The hack style seemingly adds cognitive noise here (I'm not well-versed on how Hack-style accommodates mixins currently looking out for OOP programmers)

class MyElement extends HTMLElement |>  connect(store)(#) |> someOtherMixin(#) {
}

If the hack-style proposal indeed expects that mixins are handled with it in such a way, I find it unnecessarily verbose adding cognitive noise in a matter F#-style & the minimal proposal doesn't.

@highmountaintea
Copy link

I respectfully disagree; it's even common in POSIX-compliant process buffering / terminals that accommodates anonymous pipelining:

A classic example:

ls -l | grep key | less
class MyElement extends HTMLElement |>  connect(store) |> someOtherMixin {
}

I think you are misunderstanding me. In the examples above, grep key and connect(store) are not pure unary functions nor curried-ahead functions. Instead they are curried-in-place functions.

I have no problem with using curried-in-place functions in pipeline examples, but I have problem with people using ONLY unary and curried-ahead functions in pipeline examples.

@lozandier
Copy link

lozandier commented Apr 29, 2021

@highmountaintea Gotcha; it's a matter of habit of being terse with hypotheticals; I could've written something like

class MyElement extends HTMLElement |>  connect(store) |> mergeFeatures(?, Feature1, Feature2) |> aliasElement('some-new-element-name', ?) {
}

@highmountaintea
Copy link

highmountaintea commented Apr 29, 2021

To be more specific, syntax is important in a pipeline discussion.

So it's not good to start with a fake example like below, then use it to argue which proposal has better syntax:

let r = x
  |> a
  |> b
  |> c

First of all, it's hard to visualize what real world example it's corresponding to. Second, syntax might look nice for unary functions, but terrible for HOFs.

Again, not arguing which proposal is better, but simply saying don't use fake unary function examples x |> a |> b |> c in the discussion because syntax is important.

@lozandier just to clarify, my original comment was in answer to somebody else, not specific to any of your comments

@benlesh
Copy link

benlesh commented Apr 29, 2021

Hack pipeline's power is its flaw

IMO, the hack pipeline, as proposed here is a fundamentally flawed design. It frees up people to write things in ways that make them even harder to read. If you have |> with the "magic # character", then all of a sudden, every use of |> will require the developer to read through an unknown number of tokens looking for every instance of #.

It will just encourage really weird/bad patterns:

Where's the magic #? Is there more than one?

someThing |> myFunc(1, 2, 3, 4, 5, /* ... */ 99, #, 101, 102, 103, /* ... */  392, #, #, 'lol')

What am I doing with the magic #?

someThing |> whatever(Math.random() * Math.min(# + 1, # / 200))(#)(#)

The simple pipeline proposal solves this issue by implicitly passing the value in one and only one way. Meaning that every developer always knows right where to look to see where the value was "piped" to. No surprises. It's a solid design because it's less powerful.

Rather no |> than Hack pipeline

Honestly, I'd rather have nothing at all than the hack-pipeline. It doesn't solve any use case I have, and it will mean we'll never get a good pipeline operator.

Current RxJS:

someObservable.pipe(
  map(x => x + x),
  filter(x => x % 2 === 0),
  concatMap(n => interval(n).pipe(take(3))),
)
.subscribe(console.log)

RxJS With The Desirable |> operator:

someObservable
  |> map(x => x + x)
  |> filter(x => x % 2 === 0)
  |> concatMap(n => interval(n) |> take(3))
  |> subscribe(console.log)

RxJS with the undesirable |> operator:

someObservable
  |> map(#, x => x + x)
  |> filter(#, x => x % 2 === 0)
  |> concatMap(#, n => interval(n) |> take(#, 3))
  |> subscribe(#, console.log)

It's so verbose and makes the usage of our operator functions more error prone, IMO. As there are more arguments that need to be passed.

@benlesh
Copy link

benlesh commented Apr 29, 2021

Ha! In fact, while typing up my "undesirable" operator example, I made a typo where I'm not even sure how it would work (because of the added complexity of the $ token). I took a screenshot of it before I fixed it, because it's a "WTF would this even do?" thing.

image

(The next trick is finding it)

(Magic $!!)

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Apr 29, 2021

@benlesh Thanks for sharing. Two quick comments:

  1. Generally, the topic token will probably not be something like a $ because that's currently a valid variable, but something mroe like #, ?, or % (or something else) that's explicitly not a variable. You might be interested in Tab's essay & the current hack proposal. The comment you referenced is out of date.
  2. Your (edit: second, typo'd) "undesirable" example would syntax error because there's no topic token on the RHS of that pipeline.

@benlesh
Copy link

benlesh commented Apr 29, 2021

@mAAdhaTTah I've replaced $ with # in my example so people don't get distracted by that detail.

As for your point 2. You are mistaken... which is funny, because it precisely proves my point that the Hack-style pipeline is a bad design because of how it hurts the understandability and readability of the code. Here's a screenshot with the tokens circled in red:

image

It's my strong opinion that the Hack-proposal shouldn't even be considered, and if it's the only proposal in contention, the entire proposal should be scrapped just to save room for another shot at getting it right in some other generation. I'm willing to be convinced otherwise, I'm just not sure how that's possible, given my contention with the Hack-style pipeline is specifically how wild you can get with its token and its design.

@mAAdhaTTah
Copy link
Collaborator

@benlesh Sorry, my comment was referring to the missing topic token in concatMap in the typo'd version, which would be a syntax error, rather than confusing the dev with its behavior.

@benlesh
Copy link

benlesh commented Apr 30, 2021

@mAAdhaTTah is it though? If so, why? Technically, there is one within the context of the other: |> concatMap(n => interval(n) |> take(#, 3)) or to look at it more cleanly: |> a(n => b(n) |> c(#, 3)) ... what makes it a syntax error? The presence of the other |>? How is that supposed to be obvious to the developer without some sort of IDE assistance?

And therein lies another point in favor of the simple pipeline, it's harder to create syntax errors. You're guaranteed to have passed the value along implicitly. No "forgetting" where it goes. And if people really want that power, they can still do it:

// Hack version
foo |> bar(#, 1, #, 3, #)

// Simple version
foo |> $ => bar($, 1, $, 3, $)

And what about other scenarios? foo |> bar(1, 1, 3, 4, 5, #), and you're like no, wait: foo |> bar(1, 1 |> # + #, 2, 3, 4, 5, #). The # character fundamentally hurts readability.

// Hack version
foo |> bar(1, 1 |> # + #, 2, 3, 4, 5, #)

// Simple version
foo |> (fooResult) => bar(1, 1 |> n => n + n, 2, 3, 4, 5, fooResult)

I suppose many parens are required there? So let's try it:

// Hack version
foo |> bar(1, (1 |> # + #), 2, 3, 4, 5, #)

// Simple version
foo |> (fooResult) => bar(1, (1 |> n => n + n), 2, 3, 4, 5, fooResult)

Even with the parens, I find the #, # ,# of the Hack example to be harder to read. It lacks the ability to just give it a variable name.

I fail to see the advantage of Hack pipeline while the disadvantages are very clear to me.

@Pokute
Copy link
Contributor

Pokute commented Apr 30, 2021

@benlesh I know that it's not a complete solution since we want to support editors/users who don't have all JS tooling, but good IDE will help immensely. You can see the type and code for the token:
You can see the type and code for the token.
You can also use Go to definition which highlights correct code when used on the # token
Go to definition highlights correct code for that token

Finally, it also shows the correct context for nested tokens too:

@mAAdhaTTah
Copy link
Collaborator

@benlesh

is it though? If so, why?

Because the RHS of the pipeline operator has to use its topic token or it's a syntax error. On the line beginning with concatMap, the second operator uses its topic token, which means the first one doesn't.

To be clear, I'm team F#, just tryna be accurate.

@loreanvictor
Copy link
Contributor

To be more specific, syntax is important in a pipeline discussion.

So it's not good to start with a fake example like below, then use it to argue which proposal has better syntax:

let r = x
  |> a
  |> b
  |> c

First of all, it's hard to visualize what real world example it's corresponding to. Second, syntax might look nice for unary functions, but terrible for HOFs.

Again, not arguing which proposal is better, but simply saying don't use fake unary function examples x |> a |> b |> c in the discussion because syntax is important.

I suspect I should point out, I was using such an example to outline increased visual noise in the hack-style, where having fewer number of symbols / elements helps greatly with clarity: it is easier to see how boilerplatey (#) can get in

x |> a(#) |> b(#) |> c(#)

compared to

myObservable |> filter(x => x % 2 === 0)(#) |> debounceTime(200)(#)

Note that the number of important elements in both cases are kind of the same, but the point is harder to see in the later example. Of course, that abstraction / simplification, while it magnifies some aspects of the proposal, it can miss another. It does not mean that those points are necessarily overlooked (in the same comment for example I did bring up the very same "curry-in-time" issue too), its just that it sometimes helps to have examples that focus on one aspect of the syntax rather than others.

@loreanvictor
Copy link
Contributor

loreanvictor commented Apr 30, 2021

Because the RHS of the pipeline operator has to use its topic token or it's a syntax error. On the line beginning with concatMap, the second operator uses its topic token, which means the first one doesn't.

Guess I'm getting old, but this wasn't easy to figure out honestly. So in this case for example:

f(a) === a |> f(#)
     === f |> #(a)
     === a |> (# |> f(#))
     === f |> (a |> #)

All the statements are correct and equivalent, except the last one, which is due to # in every pipeline basically (and implicitly) shadowing # in the outer-scope (in this case, the # belongs to the inner-pipeline not outer-pipeline, meaning the outer-pipeline does not have a # which constitutes a syntax error), right?

EDIT: also got the third statement wrong initially, but I think thats because I am really used to think about pipe as in x |> f kind of form.

@mAAdhaTTah
Copy link
Collaborator

Guess I'm getting old, but this wasn't easy to figure out honestly.

I wouldn't necessarily chalk it up to being old lol. I've been staring at pipeline examples for the better part of 3 years now.

All the statements are correct and equivalent, except the last one...

Yup, that's correct.

@kosich
Copy link

kosich commented May 1, 2021

To prove minimal proposal extensibility, I created a babel plugin that allows minimal 42 |> console.log syntax, along with a hack-like style 42 |> _ + 1 |> console.log.

And it can be useful on it's own:

let addOne = _ + 1;
// equals
let addOne = x => x + 1;

Try it in the babel playground.

More details in #186.

@tabatkins
Copy link
Collaborator

Closing this issue, as the proposal has advanced to stage 2 with Hack-style syntax.

@treybrisbane
Copy link

https://youtu.be/gSY7XH_WW8U

Memes aside, I truly do think this is a mistake. 😢

Very interested to read the TC39 Meeting Notes on this one. Hopefully they shed some light on this seemingly out-of-the-blue decision.

@aadamsx
Copy link

aadamsx commented Sep 9, 2021

I knew he would push hack style even if fsharp was the better. Hack style is a mistake for JS, I won’t be using it.

Where is the announcement and explanation of reasoning behind this decision by @tabatkins? Why the lack of communication? This is not the way to lead an proposal.

This is a big, years long, disappointment.

@treybrisbane
Copy link

@aadamsx I understand the disappointment and frustration, but let's not make things personal. It's not like Tab is on a personal crusade to crush anyones hopes and dreams.

I don't know what next steps are at this point, but whatever they are, let's try to keep things constructive. 🙂

@benlesh
Copy link

benlesh commented Sep 9, 2021

There were unaddressed concerns about the hack pipeline in this issue. The proposal being in stage 2 with the hack pipeline is irrelevant and this should have have been closed.

@aadamsx
Copy link

aadamsx commented Sep 9, 2021

It’s an attempt to shut down the conversation @benlesh — he’s closed many unresolved issues today.

@mnn
Copy link

mnn commented Sep 9, 2021

Such disappointment. I guess I keep using custom pipe function instead of verbose non-functional pipe operator... 😞

@anatoliyarkhipov
Copy link

How do you call that in your countries guys, when some group of people at the top, not subject to any community control, makes non-transparent decisions for their own reasoning, affecting millions of people? Hmm. To make it absolutely clear, the next logical step would be to lock this conversation with some excuse like "too heated" or whatever.

@jamiebuilds
Copy link
Member

jamiebuilds commented Sep 9, 2021

I'm going to ask that people communicate their concerns and criticisms of this proposal without making personal attacks. This was not the decision of a single person, the Hack-style pipelines proposal was presented at the last TC39 meeting and the committee reached consensus to move to Stage 2.

@tc39 tc39 locked as too heated and limited conversation to collaborators Sep 9, 2021
@tabatkins
Copy link
Collaborator

I'm going to go ahead and lock this thread; the comments since I closed this issue have been solely personal ire directed at me or otherwise off-topic. (And "inb4 lock because too heated" is never a productive comment.)

This proposal has been active since 2018, and in those three years we've iterated over practically every syntax possibility for this feature. (Curiously, the Elixir-style pipeline received only cursory treatment, tho, despite it being a good match for JS's standard arglist organization.) In that time we hadn't reached consensus on a syntax proposal, and importantly hadn't convinced the TC39 committee that the proposal itself was sufficiently worthwhile to advance past stage 1. Over the last year I, and several others, have advocated for the Hack-style syntax, and produced what were apparently relatively compelling arguments for our position (see my essay, and this proposal's current README); as a result, the majority of TC39 members directly interested in this proposal either began to support this syntax, or at least considered it an acceptable outcome. This also ended up getting over the objections of the rest of the committee, such that at this last meeting the committee agreed to advance it to Stage 2.

Decisions can be changed. Stage 2 still allows for significant changes if necessary, tho ideally any major concerns have been dealt with prior to this. I invite further debate, but caution y'all that we've seen and considered a lot of arguments and angles already; please read the closed issues in this repo. Repeats of previous arguments aren't going to be productive, but any new information is welcome.

For example, one of the major bits of reasoning cited in my essay is that the pipe operator is useful for linearizing any nested expression, not just nested function calls. I looked at the set of use-cases that could benefit from linearization by pipes, estimated the usage of the various categories, weighed the cost/confusion imposed on each category by each alternative, and found Hack to be the best. Evidence that my rough estimates of category size will be way off in actual usage (like, a convincing argument that this operator really will only (or at least a strong mostly) be used by HOF libraries rather than other code) could sway things.

Ultimately, tho, for this proposal to progress a decision has to be made, and any decision will disappoint some number of people. My goal is to ensure that in the long term my actions disappoint the least number of people by the least amount, and I think I'm on track for that. If you disagree, and feel that you have new arguments that haven't already been gone over in previous issues (or perhaps an old argument that was unfairly passed over) please open a fresh issue.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests