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

Pipeline Placeholders #75

Closed
gilbert opened this issue Dec 13, 2017 · 110 comments
Closed

Pipeline Placeholders #75

gilbert opened this issue Dec 13, 2017 · 110 comments

Comments

@gilbert
Copy link
Collaborator

gilbert commented Dec 13, 2017

I'd like to explore an alternative to the partial application proposal that would only be used in conjunction with the pipeline operator.

There has been discussion around using ? as a placeholder for functions with multiple arguments. Consider the readme example:

let person = { score: 25 };

let newScore = person.score
  |> double
  |> (_ => add(7, _))
  |> (_ => boundScore(0, 100, _));

newScore //=> 57

If the ? were to be built into the pipeline operator, we could rewrite the example to the following:

let newScore = person.score
  |> double
  |> add(7, ?)
  |> boundScore(0, 100, ?);

and since this placeholder is scoped to the pipeline operator, it has the potential to be more expressive:

let ex1 = person |> double(?.score);
let ex2 = person.score |> add(?, ?);

and potentially allow member expressions?

let x = 10 |> f |> ?.foo |> g;

On the other hand, any sort of ? placeholder might have people let write strange things like
x |> f(? ? ? : 10). Is this problematic?

@jEnbuska
Copy link

jEnbuska commented Dec 13, 2017

It could be a bit confusing

function plus(number1, number2=1){
  return number1+number2
}
function curringMultiply(target){
   return (multiplier) => target*multiplier;
}
const a = 10;
2 |> addition(?); // 3
2 |> addition(?, a) // Error?
2 |> it => addition(it, a) //12
2 |> curryingMultiply(a) // what happens here...
2 |> curryingMultiply(?) // ... or here
2 |> (it) => curryingMultiply(a)(it) // 20

@littledan
Copy link
Member

@gilbert This is an interesting idea. I'm not sure if we're ready to forclose on partial application completely, even if it got somewhat chilly reception the first time around (lots of proposals do); I'd like to form this as a strict subset of what could grow into a partial application proposal if it's picked up again. So far, though, this sketch seems without any particular contractions, except for the ?. part, which is an unfortunate overlap with the optional chaining operator.

@jEnbuska I don't really see what's ambiguous about the semantics in those cases. Could you explain what the confusion is? I could write out the semantics I'm imagining

cc @rbuckton @bterlson

@jEnbuska
Copy link

jEnbuska commented Dec 14, 2017

@littledan I just wasn't syntax, but I think I get it now.

@Alexsey
Copy link

Alexsey commented Dec 25, 2017

@gilbert What should we get from limiting usage of partial application to pipleline operator? All examples you provide could exists without this limitation

@littledan
Copy link
Member

@Alexsey A worry from TC39 is that partial application is too confusing, syntactically and otherwise, in general. Maybe the restriction to just this pace could make it easier to understand.

@davegregg
Copy link

davegregg commented Dec 25, 2017

@Alexsey It's a bit like a shorthand for arguments[0], or possibly a "last value" accessor. Here are some imaginative examples:

A: const print = function { console.log(?) }; print('Hello, world!');

B: { console.log(?) }('Hello, world!')

C: for (myArray) { console.log(?) }

D: myArray.map(val => ? + value)

But I have problems with these which make me want to say that this syntax should be scoped to the pipeline:

  1. Ambiguity with potential other uses, e.g. ternaries, optional chaining, null coalescing -- particularly when compounded with optional whitespace and leading zeros in decimals.

  2. These uses don't look that useful and the "last value" accessor concept could be a real performance issue.

@Alexsey
Copy link

Alexsey commented Dec 26, 2017

@davegregg The examples you provide are out of the scope of partial application proposal as well as out of scope of discussion of limiting partial application usage to pipeline operator. They are like about usage of partial application in function bodies. There is already a discussion on that in the partial application repo

@trustedtomato
Copy link

@gilbert I've made a proposal which would enable your suggestions. According to that proposal, your examples could be rewritten as:

let newScore = person.score
  |> double
  |> #add(7, ?)
  |> #boundScore(0, 100, ?);
let ex1 = person |> #double(?.score);
// I'm not sure what you meant here, but tried to guess
let ex2 = #(person.score |> #add(??, ?));
// But without pipeline, it would be more pretty
let ex2v2 = #add(person.score, ?)); // Note that the evaluation if deferred
let x = 10 |> f |> #?.foo |> g;

@gilbert
Copy link
Collaborator Author

gilbert commented Dec 28, 2017

What should we get from limiting usage of partial application to pipleline operator? All examples you provide could exists without this limitation

We would get a removal of ambiguity without needing to introduce another syntactic marker.

@Alexsey
Copy link

Alexsey commented Dec 29, 2017

@gilbert Ok, that sounds like an argument. So may be discussion should be about allowance of not using syntactic marker for partial application in the the pipelines? I think it could be a good addition to the discussion you have mentioned in your post

@rauschma
Copy link

rauschma commented Jan 15, 2018

I think this is a great idea! IMO, this is the main use case for partial application. Other than that, arrow functions work perfectly.

This is how Facebook’s Hack does it ($$ instead of ?):

function piped_example(array<int> $arr): int {
  return $arr
    |> array_map($x ==> $x * $x, $$)
    |> array_filter($$, $x ==> $x % 2 == 0)
    |> count($$);
}

One nice trait of this extension is that it doesn’t affect code written for the proposal as it currently exists. It is completely optional.

@littledan
Copy link
Member

littledan commented Jan 20, 2018

@rbuckton @bterlson What do you think of this direction for the pipeline operator? I don't want to step on your toes in your work on the partial application proposal.

@zenparsing
Copy link
Member

zenparsing commented Jan 21, 2018

I first thought this idea should be limited to just argument placeholder positions, but it's interesting to think of ? in this context as variable binding scoped to the expression on the right hand side of |>.

One of the issues I see with the pipeline operator is that it doesn't integrate well with normal method chaining. If I'm doing some operations on arrays, for instance, I might want to chain using some built-in array methods and some functions coming from another library. Currently I'd have to use arrow functions:

I'm not very good at these examples...

anArray
 |> pickEveryOther
 |> (_ => _.filter(...))
 |> shuffle
 |> (_ => _.map(...));

This feels awkward and unfortunate.

With a placeholder binding, perhaps I could do something like:

anArray
  |> pickEveryOther
  |> ?.filter(...)
  |> shuffle
  |> ?.map(...);

Given ternary expressions (and the other potential uses for ?), I don't think ? is going to work though. Are there any other tokens that might work better?

@littledan
Copy link
Member

There are a lot of open questions about how pipeline placeholders should work. In addition to the receiver position, a placeholder could also go in nested function calls, in an object literal and as an argument to a binary operator. The partial application proposal decided to scope all of these out, and this was a point of criticism for that proposal. We'll have to think carefully about many cases as part of deciding which token makes sense.

@zenparsing
Copy link
Member

One issue that I see with placeholders is that you'd have to switch between "partial expression" mode and "normal" mode based on whether there are any placeholders (which could be well on down the garden path).

It would be strange (at first glance) that these two lines would behave so differently:

val |> fn(_);
val |> fn(?);

So it seems that we may have to choose between either the current form (where the RHS is implicitly called with the LHS) and a Hack-like form, where the RHS is evaluated with the LHS as a special binding scoped to the RHS expression.

The Hack-like form has some advantages:

  • Multiple arguments are ergonomic
  • Normal method chaining is ergonomic
  • No function calls without argument lists
  • No need for the obligatory _ => arrow function

The disadvantage is that it's more typing for the one-arg-call case.

@zenparsing
Copy link
Member

zenparsing commented Jan 21, 2018

It also looks like the grammar can be significantly simplified with Hack-style pipelining:

  • There's no longer any need to support arrow functions without parenthesis since the variable binding already does that for you (and no need for those closures, as well).
  • There's no longer any need for special await casing. It just works.

Pretending that $ is the LHS binding token:

val |> await $;

@zenparsing
Copy link
Member

Expanding the previous example and using Hack-style pipelining:

anArray
  |> pickEveryN($, 2)
  |> $.filter(...)
  |> shuffle($)
  |> $.map(...)
  |> Promise.all($)
  |> await $
  |> $.forEach(console.log);

Everything seems to fit together intuitively and without any magic: multiple arguments, normal method chaining, await. I would totally use this over the old function bind syntax.

@rbuckton
Copy link
Collaborator

rbuckton commented Jan 21, 2018

which could be well on down the garden path

I'm not particularly swayed by the "garden path" concern. There are already two features introduced in ES2015 that have the same concern, yet are still highly valuable:

// parenthesized expression...
(a = 1, { b }, c, [d, e]) 

// but add `=>` its now an arrow function
(a = 1, { b }, c, [d, e]) => x

// object literal expression...
({ a, b: { c, d }, e: [d, e] })

// but add `=` and now its destructuring
({ a, b: { c, d }, e: [d, e] } = x)

// array literal expression...
[a = 1, [b, c], { d, e }]

// but add `=` and now its destructuring
[a = 1, [b, c], { d, e }] = x

That said, any feature that does introduce a "garden path" concern needs to be valuable enough to outweigh concern.

I don't want to step on your toes in your work on the partial application proposal.

We could introduce ? as part of pipeline without blocking partial application if we restrict it to a very specific set of capabilities:

  • ? can only be used directly within Arguments and not just anywhere in an expression:
    • x |> f(?) - ok
    • x |> f([?]) - not ok
    • x |> ? + 1 - not ok
  • ? can only be used in the Arguments of a CallExpression that is the immediate right-side of a pipeline:
    • x |> f(?) - ok
    • x |> o.f(?) - ok
    • x |> (o.f)(?) - ok
    • x |> (f(?)) - not ok
    • x |> f(g(?)) - not ok
    • x |> f(?).p - not ok
    • x |> new C(?) - not ok
  • ? can only be used once in the argument list:
    • x |> f(?) - ok
    • x |> f(1, ?) - ok
    • x |> f(?, ?) - not ok
  • If ? is present in Arguments, the right-side of the pipeline is called with the value of the left-side in the placeholder position. If ? is not present in Arguments, the right-side of the pipeline is evaluated and called as a function with the left-side as the sole argument:
    • x |> f is effectively the same as f(x)
    • x |> f() is effectively the same as f()(x)
    • x |> f(1) is effectively the same as f(1)(x)
    • x |> f(?) is effectively the same as f(x)
    • x |> f(1, ?) is effectively the same as f(1, x)

These restrictions would still provide significant value for pipeline and would give plenty of room for partial application to proceed (assuming no syntactic marker to avoid the "garden path" concern).

@rbuckton
Copy link
Collaborator

rbuckton commented Jan 21, 2018

One option for passing the LHS as the this receiver would be to not use a placeholder at all:

anArray
  |> pickEveryN(?, 2)
  |> .filter(fn)
  |> shuffle(?)
  |> .map(fn)
  |> Promise.all(?)
  |> await
  |> .forEach(console.log);

The syntax `.` Identifier isn't currently legal at the start of an Expression (ignoring ASI/LineTerminator issues for now). For element access we could use `.` `[` Expression `]` (similar to how optional chaining uses `?` `.` `[` Expression `]`).

Awaiting in a pipeline could be done via a syntax like `|>` `await`, which indicates that the LHS should be awaited before continuing.

Something like:

  • LeftHandSideExpression `|>` `await` - Await the LHS
  • LeftHandSideExpression `|>` `.` Identifier - Pipeline LHS property access
  • LeftHandSideExpression `|>` `.` `[` Expression `]` - Pipeline LHS indexed access

@zenparsing
Copy link
Member

zenparsing commented Jan 21, 2018

@rbuckton You could do that, but it's a pretty high cognitive burden to place on users that just want easy left-to-right chaining. With Hack-style pipelining, you don't need any of those special-case grammars.

The fact that Hack-style pipe doesn't need any extra syntax to cover all of our use cases indicates to me that partial application is not the right problem to solve here. It may be a useful thing on its own, of course, but it's orthogonal to the needs of a pipeline operator.

The reason that partial application seems like a good fit for pipeline is that the current proposal has a "magic" function call (magic in the sense that there is no argument list) and partial application allows you to program around that magic function call.

@rbuckton
Copy link
Collaborator

If we accept the above restrictions for ?, and restrict partial application to direct Arguments, partial application could be extended to allow ?.x (or ?.x()) for binding this as _ => _.x.

However, there's no meaningful way to handle await with partial application, so val |> await ? isn't viable. I'd still prefer to see |> await on its own as a special-case because it's behavior can't be replicated with an arrow in the same position, since val |> await |> f(?) is effectively (await val) |> f(?) which is effectively f(await val).

Another option is an implicit await in any pipeline in an async function (similar to yield* in an async generator), however that could lead to poor performance and unanticipated behavior (e.g. wanting to pipe the promise itself into a function). I prefer the explicit await.

@gilbert
Copy link
Collaborator Author

gilbert commented Jan 21, 2018

Hack-style placeholders look promising! I like the idea of "enforcing" them. Here are some issues I can see so far:

(1) Considering the x |> await ? |> f example, |> await will still need its own special case. Otherwise this example will (I think) be parsed as x |> await (? |> f).

(2) Arrow functions are still useful for collecting variables. For example:

getConfig()
  |> setDefaults(?)
  |> c => f(c,10)
  |> g(c, ?)

Notice how there's no placeholder needed in the second pipe. How would this be handled?

(3) Using curried functions looks a bit awkward, e.g. x |> map(f)(?). It's worth mentioning since the pipeline op had a lot of support from the FP community.

@gilbert
Copy link
Collaborator Author

gilbert commented Jan 21, 2018

@rbuckton could you explain what you mean by "there's no meaningful way to handle await with partial application" ? I don't think I understand the issue.

@kurtmilam
Copy link

kurtmilam commented Jan 21, 2018

Hack style looks alright to me, with the exception that I don't want to be forced to use a placeholder if the thing on the right-hand side of the operator is a function or method that doesn't need to be partially applied.

In other words, I think I'd be pretty happy with this syntax (recycling zenparsing's previous example):

anArray
  |> pickEveryN($, 2)
  |> $.filter(...)
  |> shuffle // no placeholder required here
  |> $.map(...)
  |> Promise.all // ditto
  |> await $
  |> $.forEach(console.log);

@rbuckton
Copy link
Collaborator

rbuckton commented Jan 21, 2018

Partial application always returns a function, but await is local to the current function. You couldn't, for instance, write the following and have it do anything meaningful:

async function f(v) {
  return await ?;
} 
f(p).then(g => {
  // not async
  const x = g(Promise.resolve(1));
  // what is x? 
});

If we want to avoid having placeholders here block partial application, then we shouldn't leverage placeholders with await.

@mAAdhaTTah
Copy link
Collaborator

@kurtmilam Maybe I'm wrong, but I don't think the $ works because it's a valid variable name. It's not clear in any of those cases whether the functions should be called immediately or pipelined.

@kurtmilam
Copy link

@mAAdhaTTah , the dollar sign wasn't my choice. Rather, I carried it over from the example in this comment.

It's my understanding that all of the functions in the original example are to be pipelined, and that was my intention, as well.

@zenparsing
Copy link
Member

@gilbert

  1. Await is a unary expression and has a pretty high precedence, so val |> await ? |> f(?) should parse correctly. No?
  2. I'm not sure I quite understand the example. Could you explain it a bit (or give an intended desugaring)?
  3. True, dropping the implicit call will make the pipe operator less optimal for some functional programming patterns. But Javascript is multiparadigm and OO/hybrid programmers need a solution for left-to-right chaining as well. I think we should consider and balance all these different needs when designing syntax.

@gilbert
Copy link
Collaborator Author

gilbert commented Jan 22, 2018

@zenparsing

  1. Oh really? Great 😄
  2. Here's the desugaring:
getConfig()
  |> setDefaults(?)
  |> c => f(c,10)
  |> g(c, ?)

setDefaults( getConfig() )
  |> c => f(c,10)
  |> g(c, ?)

let c = setDefaults( getConfig() )
f(c, 10) |> g(c, ?)

A bit contrived, but it demonstrates how you can gather arguments using an arrow fn.

@rbuckton Ohh I see, you're saying a pipeline-placeholder version of this proposal should not allow the await ? syntax because it would be generally incompatible with a future partial application proposal syntax?

@rbuckton
Copy link
Collaborator

rbuckton commented Feb 1, 2018

I'm a bit wary about mixing pipeline styles between |> and |: and using Hack-like behavior:

anArray
  |: pickEveryN($, 2)
  |: $.filter(...)
  |> makeQuery($) // oops, used the wrong operator.

I'd love to see F#-style and partial application advancing, but we still need to consider dot-properties. Corner cases like x |: $.length are still solvable via arrows (e.g. x |> ($ => $.length)), so its more of an inconvenience than a blocker.

I've been wondering if partial application should have a form like (?).prop and (?).method(), since its visually aligned with f(?) (since the ? is in parens), and would help it to be visually distinguished from optional chaining (e.g. (?)??.method()).

@pygy
Copy link
Contributor

pygy commented Feb 1, 2018

@dallonf I agree with you, hence my proposal in #23 to make |> and => play better together (no parens in the common case).

@rbuckton
Copy link
Collaborator

rbuckton commented Feb 2, 2018

If we are considering |> await as a special syntax, we could consider the following as well:

`|>.` Identifier
`|>[` Expression `]`

// or (allowing whitespace)

`|>` `.` Identifier
`|>` `[` Expression `]`

Basically making |>. a . with a lower precedence. Combined with partial application you could have this:

anArray
  |> pickEveryN(?, 2)
  |>.filter(fn)
  |> makeQuery;

Which would be the same as this:

(anArray |> pickEveryN(?, 2)).filter(fn) |> makeQuery;

It lines up visually with |> and |> await and gives us the flexibility to support optional chaining as well.

@js-choi
Copy link
Collaborator

js-choi commented Feb 2, 2018

@rbuckton: With regard to accidentally using F#-style/tacit/call pipe where a Hack-style/placeholder/binding pipe would have been appropriate, at the very least the bug could be immediately detected during compilation, if the placeholder is not a valid variable identifier. That is, if the F#-style pipe is |> and the Hack-style placeholder is ^^, then x |> f(^^) would be an immediate SyntaxError.

If the placeholder is chosen to be a valid variable identifier like $ (which I do not think is a good idea for reasons like this), then the bug may still fail quickly with a ReferenceError if $ is not already defined in the outer context.


@littledan: The remaining questions that you listed should probably each get its own issue, right? For instance, the question of the Hack-style-placeholder has been getting discussion in #84, but it may deserve its own thread.


@kurtmilam, @mAAdhaTTah: Ah. I see now how the inlining of one-off arrow functions in F#-style pipes could be trivial. It would still force the programmer to rely more on compiler magic to reason about their programs’ memory, though. And the big point of having both F#- and Hack-style pipes is to reduce magic. By “magic” I mean giving special treatment to an edge case, and by “uniformity” I mean the absence of magic and special casing, like how Hack-style pipes could be transformed into nested lexical blocks (like do expressions), using the same simple procedure without any branches for edge cases.


@dallonf: I sympathize with the worry about ASCII-symbol soup. The solution is obviously to go the Perl 6 route and start using non-ASCII Pattern_Syntax Unicode characters, as the private-field proposal’s FAQ mentions. This is mostly a joke.

But, yes…Readability that new syntaxes may afford must be balanced with the readability that too many symbols might compromise.

@js-choi
Copy link
Collaborator

js-choi commented Feb 2, 2018

Come to think of it, there’s another use case that Hack-style/placeholder/binding-style pipes would cover that arrow functions with F#-style/tacit/call pipes would not. yield and yield * would not work within nested functions, just as with await. Surely special-casing not only |> await but also |> yield and |> yield * would not be desirable. Though I suppose that a |>[ expression ] syntax could work, too…but isn’t that just a Hack-style pipe now? Would not the inner expression require a placeholder anyway?

Incidentally, if optional method calls (x.?y(z) or whatever the syntax ends up being) get standardized, then adding another operator for that wouldn’t visually line up with |> , |>., and |>[ anymore…and by now we’re talking about quite a few new operators.

@charmander
Copy link

Perhaps we should consider more word-based operators (like await). Some of those may break existing code, though, which is tough... although as I think somebody brought up in relation to |await>, there aren't any operators in the language that use a symbol and characters... but maybe there should be! It could leave the language more room to grow in the future - and potentially be more readable in the short term.

I'll throw out, just as a proof of concept, |pipe, |pipef (i.e. "pipe function", for the tacit version), and |px ("pipe x [placeholder]" - although even this might be too verbose for its purpose).

@dallonf x|pipe(y) can’t change meaning.

@zenparsing
Copy link
Member

@js-choi Of course, there's yield too, how could I forget!

Generically any "control-flow" type expression will fit into this category, including any possible future "statements as expressions" like return.

@mAAdhaTTah
Copy link
Collaborator

yield makes for an awkward fit with the pipeline operator, because of the way values are passed in and out of the generator function. It's seems weird to me that we'd be able to pass up a value to the caller and then get a new value down for the rest of the pipeline. Do we need to support yield? We discussed yield a bit in #66.

@jridgewell
Copy link
Member

I've brought up yield before in offline conversations, and everyone else seems to think that it's not a great fit because yield doesn't need an argument (but yield * does).

It's seems weird to me that we'd be able to pass up a value to the caller and then get a new value down for the rest of the pipeline

Isn't that just like await? I see no difference from the perspective of the function author, even if await's waiting semantics are slightly different. await is just a yield to me.

@mAAdhaTTah
Copy link
Collaborator

@jridgewell I don't think so. Let's take this example:

async function foo(data) {
  return data |> processData
    |> fetch
    |> await
    |> processResponse
}

The caller of this function passes in data and gets an async response. It cannot inject new values into the pipeline.

Taking the same example (assuming yield is valid):

function* foo(data) {
  return data |> processData
    |> fetch
    |> yield
    |> processResponse
}

If the caller does this:

const gen = foo();

gen.next(); // run to first yield
gen.next('some new value'); // pass in a new value

I think I've got this right, but maybe the first gen.next isn't necessary?

then processResponse in the pipeline gets 'some new value' instead of the return value of fetch, which I think is kind of weird.

@jridgewell
Copy link
Member

jridgewell commented Feb 2, 2018

It cannot inject new values into the pipeline.

It has replaced fetch's promise with the thenable's result. That's injecting new data. It's best explained with an example, showing how async/await it just a wrapper around generator/yield:

async function test(promise) {
  const value = await promise;
  return value + 1;
}
test(Promise.resolve(1)).then(console.log.bind(console));

// as a generator
// see co: https://github.com/tj/co
test = co(function *test(promise) {
  const value = yield promise;
  return value + 1;
});
test(Promise.resolve(1)).then(console.log.bind(console));

@mAAdhaTTah
Copy link
Collaborator

It's replaced fetch's promise with the thenable's result.

Yes exactly, in my example, foo calls fetch and await unwraps the value. To clarify, in the await example, the caller cannot inject a new value. In the generator, the caller can call gen.next(newVal) to inject a new value into the pipeline.

@jridgewell
Copy link
Member

jridgewell commented Feb 2, 2018

I think I'm arguing a finer point. What is the functional difference between halting a async function's execution to await on promise, and halting a generator function's execution to yield a value and get a result?

await promise;
// internally translates into
promise.then(value => {
  gen.next(value);
}, err => {
  gen.throw(err);
});

Async is internally just a generator. Who cares that it only unwraps the promise's result? It has halted and returned a brand-new, different value (the original value is a promise) to the function's execution. That's a generator.

If we can agree on that, I don't see why we would only special case await and not yield/yield *.

@mAAdhaTTah
Copy link
Collaborator

It's a difference in who has control over that value. When the pipeline calls down into a function that returns a promise, the pipeline has control; it calls into the promise-returning function, so it know the function will return a promise that will resolve with a specific (type of) value, based on the implementation of that function.

If a pipeline yields up, the caller of the pipeline has control; the function with the pipeline thus has no means by which to control the value that gets next'd into the pipeline. It can be literally anything, depending on what the caller wants to do, whereas the resolution value of the promise is defined by the implementation of the function that the pipeline is calling into.

If your example above, we're yielding up into co:

test = co(function *test(promise) {
  const value = yield promise;
  return value + 1;
});

If co decided to just ignore the promise resolution value and next with something completely different, it could. The generator can't control that.

Async is just a generator, but a generator is not just async. Moving into the more general generator case, I am arguing that the semantics of all the non-async use cases for generators make it a less useful candidate for use in the pipeline.

@rbuckton
Copy link
Collaborator

rbuckton commented Feb 2, 2018

If we don't have yield, yield*, or await as options we're not breaking anything, just adding a minor inconvenience:

// await:
await (data
  |> processData
  |> fetch)
  |> processResponse

// or

const temp = data
  |> processData
  |> fetch;

(await data)
  |> processResponse;

// yield:
(yield data
  |> processData
  |> fetch)
  |> processResponse

// or

const temp = data
  |> processData
  |> fetch;

(yield data)
  |> processResponse;

The same can be said for dot-properties, etc:

(anArray
  |> pickEveryN(?, 2))
  .filter(fn)
  |> shuffle

Adding |> await, |> yield, |> yield*, |> .prop, |> [expr] (and even |> ??.prop and |> ??[ expr ]) are merely added convenience (i.e. not having to wrap the LHS in ()).

Another option I mentioned in #75 (comment) might be to introduce (?) in partial application, but we could instead have |> (?) be a special syntax to "evaluate the pipeline and get the result". This would allow us to do |> (?).prop, |> (?)[expr] and |> (?)() (and also |> (?)??.prop, |> (?)??[expr] and |> (?)??()). Basically, lhs |> (?) could mean (lhs) (requiring |> (?) to have a fairly high precedence compared to |> so that it can be used in a LeftHandSideExpression).

@zenparsing
Copy link
Member

If we don't have yield, yield*, or await as options we're not breaking anything, just adding a minor inconvenience:

Using this criteria, the lack of pipeline itself is just adding a minor inconvenience. 😄

I think it's more appropriate to say that lack of await or yield support would break the left-to-right composition that the operator was supposed to solve.

@rbuckton
Copy link
Collaborator

rbuckton commented Feb 2, 2018

Using this criteria, the lack of pipeline itself is just adding a minor inconvenience. 😄

Perhaps, but pipeline adds more value than just replacing f(x) with x |> f. It changes the execution order of those expressions. In f(x) you evaluate f before you evaluate x, where in x |> f you evaluate x then you evaluate f. This is a very important semantic that can only otherwise be achieved with temporary variables. As such its more than just a convenience compared to just having to add () around your expression.

I think it's more appropriate to say that lack of await or yield support would break the left-to-right composition that the operator was supposed to solve.

I'm not discounting this at all. I think having |> await, |> yield, and |> yield* would all be valuable but not having them doesn't necessarily need to be a blocker to the proposal.

edit: I accidentally |>'d my > in the quote above 😉

@jridgewell
Copy link
Member

@mAAdhaTTah: I think we're getting too far off topic now. Let's open up a new issue to specifically discuss yield?

I think having |> await, |> yield, and |> yield* would all be valuable but not having them doesn't necessarily need to be a blocker to the proposal.

Agreed. If we do not have them in the proposal, I'd like to at least forbid them appearing (un-parenthesized) as the RHS so we can add them later.

@mAAdhaTTah
Copy link
Collaborator

@jridgewell I was about to suggest the same thing. Continued in #90.

@js-choi
Copy link
Collaborator

js-choi commented Feb 3, 2018

This issue overlaps a lot with #89, but its discussion has sprawled across many topics. Should this issue be closed in favor of #89, as @mAAdhaTTah suggests in #89 (comment)? (Also, should there be a separate issue for bikeshedding the placeholder’s spelling? >< and %% come to mind as another non-identifier alternative to ^^ and <>; %% is similar to Clojure’s %…)

@littledan
Copy link
Member

@js-choi I like both of those suggestions for issue organization--go for it!

@tabatkins
Copy link
Collaborator

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

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Oct 11, 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