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

Proposal 4: Smart Pipelines, Explainer + Spec #100

Closed
js-choi opened this issue Mar 8, 2018 · 29 comments
Closed

Proposal 4: Smart Pipelines, Explainer + Spec #100

js-choi opened this issue Mar 8, 2018 · 29 comments

Comments

@js-choi
Copy link
Collaborator

js-choi commented Mar 8, 2018

I’d like to ask for feedback/criticism on a detailed explainer and specification for Proposal 4: Smart Pipelines plus several possible extensions to it.


In particular, I’d like to know whether their current organization is too confusing. There is a simple “actual” Core Proposal, plus several optional Additional Features that extend the Core Proposal and address several other use cases.

I should stress that the Additional Features are optional and mutually independent. They show the potential of simply extending the core proposal to handle other use cases (such as composition, partial application, and method extraction). And I’ve attempted to keep the core proposal forward compatible with all of the additional features.

But I don’t want to scare people with too many new features at once, though. All of the Additional Features are optional. I’ve currently integrated the Additional Features in the same documents because the Additional Features demonstrate the conceptual generality of the Core Proposal. I could separate them into other documents if it would be better.


See also #75, #89, #91, #95, and #96.

Thanks for your time!

@js-choi js-choi changed the title Proposal 4: Explainer + Spec Proposal 4: Smart Pipelines, Explainer + Spec Mar 8, 2018
@zenparsing
Copy link
Member

zenparsing commented Mar 8, 2018

In the explainer I see the following example:

promise
|> await #
|> # || throw new TypeError()
|> doubleSay(#, ', ')
|> capitalize
|> # + '!'
|> new User.Message
|> await stream.write

Note that in JS new expressions don't need parenthesis. So this is a valid way to construct an instance:

let userMessage = new User.Message;
// equivalent to:
// let userMessage = new User.Message();

At a more general level, do you think that new expressions present a problem for F# and Smart-Mix pipelines, since other users might have the same confusion about paren-free new?

@js-choi
Copy link
Collaborator Author

js-choi commented Mar 8, 2018

@zenparsing: Once the smart body syntax’s rule is internalized, new expressions would hopefully be never confusing for Proposal 4: Smart Pipelines as currently written in the explainer/spec above.

The rules can be fully stated in three sentences:

  1. Bare-style pipelines are all pipelines whose bodies are one or more identifiers, separated by ., and optionally preceded by new or await.
  2. All other pipelines are topic-style pipelines.
  3. All topic-style pipelines must use the topic reference somewhere in their bodies.

… |> new User.Message is always bare style (that is, new User.Message(…)) because it is a chain of identifiers separated by . prepended with an optional new, which fulfills all of bare style’s criteria.
… |> new User.Message is never valid topic style because it does not contain a topic reference.
If … |> new User.Message were not valid bare style, it would have triggered the early error rule that forbids topic-style pipelines that do not contain a topic reference.

The decision was that completely nullary expressions such as new User.Message as new User.Message are almost never useful within pipelines. Instead, the goal of mitigating footguns was prioritized here. In this example, the developer would have to make explicit their desire to use a nullary constructor call by placing the nullary call in a do expression: … |> do { #; new User.Message; } |> ….

In the same way, … |> 5 |> … is an early syntax error, because it is not a bare-style pipeline, yet it does not contain a topic reference. The developer would be required to make your desire to discard the previous pipeline’s value by using a do expression … |> do { #; 5 } |> …...

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Mar 8, 2018

I don't think they present a "problem" for F#, b/c in F#, the new instance would be slotted into the pipeline, similar to putting a called function in there. If you wanted to create a new instance in the middle of the pipeline, an arrow function would be needed:

x |> x => new User.Message(x)

There aren't (and probably shouldn't be), special rules around new in F# style.


@js-choi Does your proposal require do expressions in order to fully function?

@zenparsing
Copy link
Member

@js-choi

Nevertheless, we're suggesting that new X mean something different inside a pipeline body versus outside of a pipeline body. That seems inherently confusing to me.

@mAAdhaTTah

We can't assume your paren-free arrows.

If a user wanted to feed the pipeline value into a constructor, can't we expect at least some users to try:

arg
  |> new User.Message;

This seems like a minor footgun to me.

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Mar 8, 2018

@zenparsing I'm still pushing for this as a solution to get them back. Even if we can't get them back, the response is the same.

In F#, I don't expect that to be something they'd try anymore than they'd expect this:

x |> doSomething(123)

to do something other than use the return value of doSomething(123).

@js-choi
Copy link
Collaborator Author

js-choi commented Mar 8, 2018

@zenparsing:

Nevertheless, we're suggesting that new X mean something different inside a pipeline body versus outside of a pipeline body. That seems inherently confusing to me.

For smart pipelines, the bare style is intended to be constrained special syntax that is not interpreted in the same manner that arbitrary expressions are—special, in order to optimize for three common use cases—constrained, in order to limit the additional complexity that it brings and force the developer to use topic style if they want anything other than those three use cases. The developer must opt into the special syntax.

Either or both of the new i0.i1.….iN and the await i0.i1.….iN special cases may be removed from bare style. The logic of including them is that bare style is already a special case with special behavior—one that the syntax rules demand the developer to make easily distinguishable from the general case (that is, topic style)—so the tradeoff is similar, with large benefit for those two common use cases. I felt this benefit often while rewriting the real-world use cases in the explainer’s Motivation section.

But nevertheless, either or both of the new i0.i1.….iN and the await i0.i1.….iN special cases may be removed. Their removal from bare style would require both of the expressions to be in topic style: new i0.i1.….iN(#) and await i0.i1.….iN(#).

The goals of the smart-pipeline spec, as it is currently written, agree with @mAAdhaTTah and Proposal 1: F-sharp Style Only, in that they optimize against the rare use case x |> doSomething(123) |> …. In fact, Proposal 4 makes that expression an early Syntax Error. This in turn forces the developer to make explicit what they actually want to happen.

@js-choi
Copy link
Collaborator Author

js-choi commented Mar 8, 2018

As an aside:

[@mAAdhaTTah] @js-choi Does your proposal require do expressions in order to fully function?

It does not, insofar that “fully” functioning here indicates a minimum baseline of usefulness for the developer. do expressions are a Very Nice To Have thing because they enable the embedding of if else statements, try catch finally statements, and switch statements: all frequently involved in the transformation of values. In addition, do acts as a way to create a “topic-context block”, something like Perl 6’s given block—within this block, statements may use the topic reference may be used as an abbreviation for the same value—such as the new Whatever() |> do { #.commonThing(123); ::rareAndComplexThing(321); } example from #101 (comment)). I would expect do expressions to be used as pipeline bodies so frequently, I also introduced Additional Feature BP as a compatible abbreviation for them. But Additional Feature BP is strictly optional.

do expressions are immensely useful, but they are useful in the same sense that embedding them in any nested expression in general is useful. The rules of topic style in the Core Proposal do not specially treat do expressions. do expressions are not necessary to the smart-pipelines proposal; they are additive and orthogonal.

@gilbert
Copy link
Collaborator

gilbert commented Mar 8, 2018

What about banning a bare new invocation within a non-topic pipe? e.g. ban x |> new Foo but allow x |> new Foo(#) and allow x |> new Foo().

It's only a minor inconvenience, and the error message could say x |> new Foo is ambiguous.

@js-choi
Copy link
Collaborator Author

js-choi commented Mar 8, 2018

@gilbert: That solution is possible, but the solution would invalidate a useful heuristic of the bare style: “Bare style never has parentheses (or brackets, braces, or any other operators other than new and await.” For instance, … |> x.y(z) |> … is invalid: the developer is forced to explicitly specify … |> x.y(z)(#), … |> x.y(#, z), or … |> x.y(z, #).


I did not anticipate that the special bare form for unary constructor calls (and, by extension, the special bare form for awaited unary function calls) would be at all controversial. They are optional features of the Core Proposal’s bare-style syntax, which is merely a special treatment of simple chains of identifiers and . (see also #100 (comment)). If bare constructor calls and bare awaited function calls are very controversial, I could remove them from the Core Proposal and isolate them in their own Additional Features BC and BA.

@gilbert
Copy link
Collaborator

gilbert commented Mar 8, 2018

I should be more clear. In my mind x |> new Foo() translates to (new Foo())(x). Not a common use case, but it's unambiguous.

To pipe a value into new in Proposal 1 I think an arrow function should be required to avoid further special casing.

@js-choi
Copy link
Collaborator Author

js-choi commented Mar 8, 2018

@gilbert: Indeed, x |> new F() might be reasonably interpreted by a human reader
as either new F()(x) or as new F(x).

It is for that reason that the current smart-pipelines spec makes x |> new F() an early Syntax Error.
This forces the developer to explicitly specify whether they mean
x |> new F()(#) versus x |> new F(#) (aka x |> new F).

This is analogous to how the current smart-pipelines spec also makes x |> f() an early Syntax Error.
This forces the developer to explicitly specify whether they mean
x |> f()(#) versus x |> f(#) (aka x |> f).

@gilbert
Copy link
Collaborator

gilbert commented Mar 8, 2018

This is analogous to how the current smart-pipelines spec also makes x |> f() an early Syntax Error

x |> f() is a syntax error in proposal 4? Or do you mean proposal 2?

@js-choi
Copy link
Collaborator Author

js-choi commented Mar 8, 2018

@gilbert: x |> f() is indeed an early Syntax Error, as denoted by the smart body syntax in the current explainer and the early error rules in the formal spec. I limited the bare style in this manner in an effort to avoid foot guns.

@hlehmann
Copy link

hlehmann commented Apr 6, 2018

I have two concerns:

  • it introduce a "smart" interpretation depending on if a # is use
  • # is limited to the pipeline definition

For the second point, you suggest a +> operator or we could imagine a "smart" interpretation of => (such as const inc = => # + 1.

I think a #-style function should be defined before a #-style pipeline because it could be used everywhere.
Imagine we choose +>, we can define inc = +> # + 1 and use 1 |> +> # + 1. Then we simplify |> +> because it's not very nice.

In order to keep things consistent we should make both |> and => smart or keep them minimal and create derived symbols for both.

If we keep |> and => minimal then derived symbols could be =>> and |>>:

// minimal way
const inc = _ => _ + 1
const two = 1 |> _ => _ + 1
const incs = list.map( _ => _ + 1 )

// #-style way
const inc = =>> # + 1
const two = 1 |>> # + 1
const incs = list.map( =>> # + 1 )

// smart way
const inc = => # + 1
const two = 1 |> # + 1
const incs = list.map( => # + 1 )

// very smart way (but it shouldn't be possible)
const inc = # + 1
const two = 1 |> # + 1
const incs = list.map( # + 1 )

@hlehmann
Copy link

hlehmann commented Apr 6, 2018

Could something like this be possible?

const inc = > # + 1
const two = 1 |> > # + 1
const incs = list.map( > # + 1 )

@js-choi
Copy link
Collaborator Author

js-choi commented Apr 6, 2018

Hey, @hlehmann; thanks for reading the proposal.

  • it introduce a "smart" interpretation depending on if a # is use

This is incorrect. For each step of a pipeline, its choice between topic or bare style depends only on whether the step is an identifier (or a chain of identifiers and .). If yes, the pipeline step is in bare style. If no, it’s in topic style. Note that this is not the same as depending on whether a # is used. This is further explained in the readme’s section on the smart step syntax, and the reasoning behind this is explained in the “syntactic locality” goal.

  • # is limited to the pipeline definition

I’m a little confused by this part, sorry. # is limited to pipeline steps because those are the scopes in which it makes sense at all.

For the second point, you suggest a +> operator or we could imagine a "smart" interpretation of => (such as const inc = => # + 1.

Here, you’re referring to Additional Feature PF, which I want to stress is an optional annex separate from the core proposal. Assuming that Feature PF got support from TC39, I wouldn’t want it to reuse =>. There’s hardly any way of extending => without being confusing or breaking existing code.

I think a #-style function should be defined before a #-style pipeline because it could be used everywhere.
Imagine we choose +>, we can define inc = +> # + 1 and use 1 |> +> # + 1. Then we simplify |> +> because it's not very nice.

This would not work under the current proposal’s rules, because 1 |> +> # + 1 is an invalid topic-style pipeline.

Consider that this is equivalent to 1 |> x => { return x |> # + 1; }. Using the rules of the smart step syntax:

  1. x => { return x |> # + 1; } is not a simple reference (it is neither an identifier nor a chain of identifiers with .). Therefore, it is in topic style, not bare style.
  2. x => { return x |> # + 1; } is in topic style, but it does not use the topic bound to the previous input 1. Therefore, it is a syntax error.

Similarly, with 1 |> +> # + 1:

  1. +> # + 1 is not a simple reference (it is neither an identifier nor a chain of identifiers with .). Therefore, it is in topic style, not bare style.
  2. +> # + 1 is in topic style, but it does not use the topic bound to the previous input 1. Therefore, it is a syntax error.

…Instead, you would want to use 1 |> (+> # + 1)(#), which is a valid topic-style pipeline. And which might as well be just 1 |> # + 1.

Thanks again for reading the proposal. Hopefully this answered some of your questions.

@hlehmann
Copy link

hlehmann commented Apr 6, 2018

Thanks for the clear answer, especially about all the syntax constraints.

I have been a bit confuse by the Additional Feature PF as you define at an extension of the smart pipeline (+> # + 1 becoming x => { return x |> # + 1; }).

My question is more about is +> useful ? I mean, using # feature outside of a pipeline.
If it's not making # a pipeline feature makes total sense.

If something like +> is useful, I think it should be considered first, independently to the pipeline, in a generic way such as const inc = +> # + 1, list.map( +> # + 1 ) or 1 |> ( +> # + 1 ) (|> as F-sharp only). In that case, because we have split => and +>, doing the same with the proposal 3 seems to be more consistent (this time 1 |: # + 1 becoming 1 |> ( +> # + 1 )). And again in that case I would consider replace +> and |: by =>> and |>> for readability.

But my general understanding is that +> is not really useful/wanted making the # feature a pipeline only feature.

I can oversimplify all this by:

  • if there is a +> or =>> I prefer proposal 3 with =>, =>>, |>, |>>
  • if not I prefer proposal 4 with only => and |>

@dead-claudia
Copy link
Contributor

dead-claudia commented Apr 7, 2018

@js-choi @tabatkins

Edit: Elixir's x |> f(y) means f(x, y), not f(y).(x). Updated a few things appropriately.
Edit 2: I'm doing a rework of my proposal that requires no new syntax - this alleviates some of my concerns regarding that on my end. In response, I've removed the relevant bullets.

I'm finding from talking with others that there's concerns this may be trying to do too much. They aren't quite as vocal as the rest of us, but I feel I should at least give them a voice here, to let them know they're not alone. Here's my personal opinions on the proposal at a higher level, and they're mostly expanding on existing concerns I've heard elsewhere.

  • It attempts to help with functional, method-based, and procedural chaining. This sounds nice, but the three styles of chaining are so drastically different in their call sequence that you find yourself bending over backwards with potentially very awkward syntax just to accomplish it.

    • Functional chaining: x |> f(...args)f(...args)(x) (Think: Ramda)
    • Method-based chaining: x::f(...args)f.call(x, ....args) (Think: Lodash _(coll), jQuery)
    • Procedural chaining: x->f(...args)f(x, ...args) (Think: Lodash _.map(xs, func))

    If you can pick one without making the other two awkward to do, it isn't something very many people will openly reject - keep that in mind. I also expand on this later in this comment, with some of the pros and cons for each.

  • The "simple scoping" and "semantic clarity" is no longer so simple as you start diving into it, especially as a prospective implementor.

    • When you allow x |> f and x |> f(#), it's not clear why x |> f() is a syntax error. Unlike - x ** 2, where the potential ambiguity is in only what the operator applies to (which makes it easy to tell), in x |> f(1, 2, 3), the ambiguity is whether something is even called. People will cross this up, especially those with a functional background.
    • This isn't as much of a problem with the core proposal, but it's a major issue with block pipelines. It's obvious in smaller blocks like in x |> { if (cond(#)) foo(#) else bar(#) }, but not so much with larger ones or with pipelines within them. (Think: x |> { if (cond(#)) foo(#) |> bar(#) })
  • The syntactic locality begins to fail with newlines in actual code that mix pipelines and method calls. With this, most everyone is going to expect both of these to work and for them to be equivalent, even though the second is technically a syntax error:

    // Why does this fail...
    x
    |> f
    .g()
    
    // ...while this works?
    x
    |> f(#)
    .g()
  • Semantic clarity itself starts to fall apart in the face of newlines and ternary operators, especially when method calls enter the picture. Consider this:

    // Why does this only call the method if `cond` is false...
    x
    |> cond ? foo(#) : bar(#)
    .g()
    
    // ...while this calls it always?
    x
    |> (cond ? foo(#) : bar(#))
    .g()

    This also comes into play elsewhere with nested pipelines - it's not always better to do things with an anonymous identifier, especially when you already have a "pipeline" of sorts (remember: # is still a name - it's not a name you get to choose, but it's still a name, just like x or y):

    // Bad: features counter-intuitive behavior
    function mapCacheDelete (key) {
        const result = key
        |> getMapData(this, #)
        |> #['delete']
        |> #(key); // Why does this not throw?
        this.size -= result ? 1 : 0;
        return result;
    }
    
    // Better: behavior is obvious
    function mapCacheDelete (map, key) {
        const result = key
        |> getMapData(map, #)
        |> #.delete(key) // Obviously a method call
        this.size -= result ? 1 : 0;
        return result;
    }
    
    // Best: visual clutter gone
    function mapCacheDelete(map, key) {
        var result = getMapData(this, key).delete(key);
        map.size -= result ? 1 : 0;
        return result;
    }
  • Parentheses and whitespace are common symbols everyone understands. They aren't just syntactic noise - they can also serve as a visual tool.

    • It's sometimes better to break a complex expression apart into multiple lines, especially when you're doing math.
    • It's customary to return multi-line JSX expressions in parentheses, so the start and end markers are at the same indentation level.
    • Sometimes, a name is nice to give you a landmark just to know what data you do have.
    • (re: the "'data-to-ink' ratio") Algorithms are not data. To draw a parallel, mathematics, a very mature field for dealing with these kinds of things, have come up with numerous ways to maintain this separation (functions vs sets, morphisms vs objects, etc.).
      • Also, related: there's the saying of "make things as simple as possible, but no simpler" (emphasis mine). The first step of making things simple is to not make things seem simpler than they are. Yes, I do agree with the gist of the "data looks better naked", but overgeneralizing the problem domain can obscure the inherent complexity it has. A good example of this is with the various Lisp dialects: a primitive dialect is very simple to implement (it's simple enough it's commonly used in CS programs for teaching language implementation), but Common Lisp's attempt to generalize various existing languages just kept bolting on large features (like the highly dynamic CLOS, optional type annotations, and multiple macro syntaxes) to the point it's competing with C++.
  • Trying to combine function composition and partial application into one single concept will fail. They may seem closely related on the surface, but when you dig into the math and how they actually relate to one another, there's far more differences than there are similarities.

    • At the category theory level, you can model function composition in two ways, either as a functor's "map" (as my proposal does) or as a semigroupoid's "compose" (as PureScript does).
  • Learnability may seem like a lesser priority, but intuitiveness goes very much hand-in-hand with "don't make me overthink". If something doesn't come naturally or express itself naturally, it's very much a design issue. And really, for a feature this conceptually simple on the surface (defining a pipeline), it should be expected that a newbie should not have to spend a week just learning the syntax for a feature. So learnability and intuitiveness should be a priority, not taking a back seat to things like "make my code easier to read" (which really depends heavily on being intuitive to start - if you don't get it on the first pass, your code automatically becomes less clear, and you may find yourself missing things you otherwise wouldn't).

  • This is going to be a bitch to parse, and is very ambiguity-prone (visually, if not grammatically). I've pointed out one case of this already, but there's others. (BTW, this is countering the "static analyzability" and "cyclomatic simplicity" arguments.)

    1. Consider that x |> f . g is interpreted differently from x |> f . g ( # ). You can't detect this without parsing the whole expression.
    2. Consider that x |> f . g ( ) is a syntax error, but x |> f . g ( ) ( # ) isn't. You can't detect this before the end of the statement.
    3. Consider that x |> f( # ) . g ( ) could be interpreted in two different ways, both by the reader and by the engine, and both are equivalent.

    You're looking at something closer to a lesser variant of arrow functions vs sequence expressions, not something that's trivial to parse.


Now, back to the alternatives:

  • For functional chaining:

    • Method-based and procedural chaining are easily enough addressed by simply using an arrow function. (Think: x |> f |> (xs => _.map(xs, func)))
      • A lower-precedence operator (below =>) would make it even cleaner, at the cost of requiring parentheses for the less common case of x => (x |> foo |> bar |> baz).
      • Many languages (think: OCaml~~/Elixir~~) already encourage this.
      • Engines/transpilers would be dumb not to eliminate the arrow function.
    • A partially-applied expression could help with OO/procedural calls.
      • The current proposal makes method chaining impossible, but that's trivially fixable.
    • Precedent:
      • OCaml, Elixir(Edit:) F#, and Elm all three implement this as a low-precedence operator and frequently use it.
      • It's pretty common to have a variant of this within functional languages that don't share too many genes with Haskell (like PureScript, which lacks it).
    • Medium con: it's not fully zero-cost in practice - engines haven't been historically great at eliminating closures created in other scopes, even temporary, single-use ones. It's not impossible, just not easy.
    • Weak con: ponyfills aren't easily generically adapted.
  • For method-based chaining, it's a little more mixed:

    • It'd reduce the burden of adding array/etc. methods - ponyfills would be really easy to write.
    • It's zero-cost in practice.
    • Precedent:
      • Kotlin basically implements this.
      • Swift's extension methods are commonly used for this.
      • Scala's implicits and Rust's traits can be used to emulate this.
    • Strong con: It's not quite as ergonomic to transform values inline - you have to create a wrapper function to use arrow functions, function run(f) { return f(this) }, and use it like this: x::run(x => ...)
    • Weak con: most existing helper utilities aren't designed for method-like use.
  • For procedural chaining, it's similar to method chaining, except the main concerning negative doesn't apply.

    • It's zero-cost in practice.
    • You can easily transform values inline with arrow functions
    • Many utilities already pass their data first, so it's easy to integrate.
    • Precedent:
      • Clojure has two macro variants of this: (->) (thread-first) for dealing with method calls and (->>) (thread-last) for most other methods.
      • D's and Nim's uniform function call syntax for non-methods is literally this, up to and including how arguments are handled.
      • C++ has an outstanding proposal to add almost exactly what D and Nim has.
      • Edit: Elixir's pipe operator does this.
    • Weak con: the syntax is a little awkward for inline arrow functions: x->(x => ...)()
    • Weak con: you can't use exact ponyfills, but modified variants would be easy to use.
    • Weak con: you need to do some mild gymnastics for functional, data-last libraries: x->(f(...args))()
      • This could be partially mitigated by making function calls higher-precedence than ->.

For my personal take, I say avoid the method-based chaining (it has the strongest cons and far fewer pros), but the other two are still very much viable.

@js-choi
Copy link
Collaborator Author

js-choi commented Apr 20, 2018

Thanks, @isiahmeadows, for the detailed comment. Sorry for the delay in responding. I’ve been busy out of town, and I’m unfortunately still busy with a major transition, so I’ve had to disengage for the past weeks. A lot of discussion has been happening here. That’s a good thing, and I am thankful, but it’s also overwhelming, so my apologies if I can’t respond to everything here, at least for now. I want to focus on developing the Babel plugin instead during my free time.

As a general response, I want to say that concerns are understandable, and I appreciate your bringing them up here.

There are some claims that I agree with, and some claims here that I either do not agree with or disagree with. But in any case, there is a lot here, so it will take some time for me to address all your points.

I also want to note that, until a comprehensive study on real-world code is conducted, all possible discussion about the proposals is just theoretical.

To this end, @mAAdhaTTah and I are together developing a Babel plugin. I do plan to implement many possible variations of smart pipelines, so that all of them may be tried by switching configuration options. But until that Babel plugin is written, or until someone else tests the different pipelines on a corpus of actual real-world code, all of this discussion is theoretical. I, @mAAdhaTTah, @littledan, and Yulia Startsev of Mozilla have also been discussing running usability studies on JavaScript developers or real-world code in many coding styles, though those too would not occur soon.

Later, when I have time, I will do my best to respond point by point—merely theoretically, of course. Thanks again for the detailed comment.

@mAAdhaTTah
Copy link
Collaborator

mAAdhaTTah commented Apr 21, 2018

I do plan to implement many possible variations of smart pipelines, so that all of them may be tried by switching configuration options.

If this is the plan, maybe we resolve #104 by implementing both and experimenting in a babylon fork. Then babel doesn't have to guarantee as much.

@littledan
Copy link
Member

I don't think a proliferation of lots of forks would be best here, as it would make it hard to see the cross-cutting effects of several proposals that are in flight. If we use a separate branch, I think it would be best to have one big "experimental" branch where we land all early proposal variants with different runtime flags.

@mAAdhaTTah
Copy link
Collaborator

Sorry if I wasn't clear; my intention was one fork with all the experimentation (it looks like we've got 2 potential variants for each), while babel proper just maintains the current "minimal", and we only upstream the result if / when we advance to Stage 2. We'd provide a single babel plugin, which tells babel to use our fork and exposes all the different configuration flags.

@thraidh
Copy link

thraidh commented Jul 7, 2019

I may be late to the party, because the last comment s more than a year old, but here is another idea, which was almost mentioned above, but not quite.

Part 1: x |> expr is always just (expr)(x), expr can be any expression, but it is expected to evaluate to something that behaves like a unary function

Part 2: an expression expr containing # is synactic sugar for #=>expr, where # is treated like an identifier.

The problem with part 2 is, how expr is limited. Optimally arr.map(#+1) should be arr.map(x=>x+1), but arr.map(conv_to_something(#)) should also be arr.map(x=>conv_to_something(x)). Without special-casing arr.map, the compiler could also convert it to x=>arr.map(conv_to_something(x)), which would be wrong.

If everything was fully typed, # could expand outwards until an expression is found which is in a place where a unary function is expected or possible.

I'd propose (( ... )) as a limiter for #-expression-expansion in the general case. It would also encapsulate the #, so #.map(((#+1))) would be y=>y.map(x=>x+1)

Fortunately |> can also be a limiter, so that x |> #+1 would do exactly the same as in "Proposal 4", but it would not be a special case for |>. Rather '|>' would be a special case for #-expression-expansion.

x |> f would also be the same, x |> f(1) is not allowed under "Proposal 4", but would be the same as x |> f(1)(#).

This is just an idea and not fully thought-out proposal and I expect that there are a lot of things which I did not think about, but in general I think the idea simplifies the pipeline-semantics and generalizes # to be used outside of pipelines.

@mAAdhaTTah
Copy link
Collaborator

@thraidh You should checkout JS Choi's follow-on proposals for the smart pipeline, as it would address some of those concerns: https://github.com/js-choi/proposal-smart-pipelines

@thraidh
Copy link

thraidh commented Jul 9, 2019

@mAAdhaTTah I know. I was trying to improve upon that by removing the need of a distinction between "topic style" and "bare style", thereby simplifying the pipe semantics and generalizing the # syntax, so it is possible to use it outside of pipes.

@thraidh
Copy link

thraidh commented Jul 9, 2019

Has "auto-awaiting" been discussed somewhere? I.e. any LHS of a pipe which returns a promise is always awaited? Would that even be a good idea? It would allow to use pipes instead of .then, but would complicate the case where you actually want to pass a promise through a pipe to the next element. It feels like the former case is a lot more useful than the latter, but this is not the basis for a decision. Should this be a new thread?

@mAAdhaTTah
Copy link
Collaborator

@thraidh Sorry, to clarify: I meant the "additional features", like Pipeline Functions address some of those use cases. He's designed the Smart Pipeline to be a base for a whole constellation of usages, some of which I think address some of your thoughts here? But maybe I'm misinterpreting.

Has "auto-awaiting" been discussed somewhere? ... Should this be a new thread?

Probably, yes. It hasn't been discussed anywhere I can recall.

@thraidh
Copy link

thraidh commented Jul 9, 2019

@mAAdhaTTah Thanks! I actually missed that or did not grasp the implications. With that additional feature the result would be close enough to my idea, so that further discussion would only be a matter of opinion.

@js-choi
Copy link
Collaborator Author

js-choi commented Mar 9, 2021

After talking more with @tabatkins and @littledan, we’ve decided to archive the smart-mix pipes proposal in favor of a simpler Hack-pipes proposal. The latter is a subset of the former.

A new Hack-pipes proposal is at js-choi/proposal-hack-pipes. There’s also a Hack-pipes spec. This proposal’s readme and wiki have been updated to link to these and deprecate smart mix.

There has already been work done in Babel by @xixixao in babel/babel#11600, which we can work on merging into Babel later.

@js-choi js-choi closed this as completed Mar 9, 2021
This was referenced Sep 28, 2021
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 12, 2021
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

8 participants