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

Discussion: Should F#-style pipelines support yield? #90

Closed
mAAdhaTTah opened this issue Feb 2, 2018 · 16 comments
Closed

Discussion: Should F#-style pipelines support yield? #90

mAAdhaTTah opened this issue Feb 2, 2018 · 16 comments

Comments

@mAAdhaTTah
Copy link
Collaborator

Relevant pieces from #75:
@zenparsing (#75 (comment))

@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 (#75 (comment))

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 (#75 (comment))

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

@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 (#75 (comment))

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));

That should lay out the two perspectives. Follow on comments from the above conversation can be found in #75. Some additional conversation can be found in #66.

@pygy
Copy link
Contributor

pygy commented Feb 2, 2018

I'd rather leave those unary operators (await, yield, ...) out of the proposal for now as suggested by @rbuckton and @jridgewell in #75 (in a way that's future proof), but if await is to be supported, then so should yield.

[yield] can pass new values

The same pipeline could see different Promise values being fed to |> await (e.g. in a loop), resulting in different values down the line the same way |> yield can pass down different values.

yield is possibly more important to support, since it can't be inlined in a pipeline using an arrow function: x |> f |> await |> g can be expressed as x |> f |> (_ => await _) |> g, which wouldn't fly for yield. OTOH I'd find it weird to yield in the middle of a pipeline series...

Edit: fI see that @js-choi also mentioned return as a candidate for "statements as expressions", would that be as a part of the do {} proposal (in which case it would not be a concern here), or is there something else I'm not aware of? While await and yield both transfer the control flow to another function, it comes back (if it ever does) exactly where it left whereas return, break and continue either leave the current function definitely or jump elsewhere in the local scope... They don't seem relevant at the RHS or even sandwiched in a pipeline (except inside do expressions, theoretically).

@gilbert
Copy link
Collaborator

gilbert commented Feb 2, 2018

x |> f |> await |> g can be expressed as x |> f |> (_ => await _) |> g

This wouldn't work since (_ => await _) would await that inner arrow function. It would also throw a syntax error since it's not an async arrow function.

@pygy
Copy link
Contributor

pygy commented Feb 2, 2018

Oh, yes, my bad, time to go to bed :-)

@jridgewell
Copy link
Member

jridgewell commented Feb 2, 2018

This wouldn't work since (_ => await _) would await that inner arrow function.

You could make it an async arrow, but that would end up propagating promises down the chain.


As I mentioned in #75, I see async/await as just being a special form of generator/yield. Specifically, Babel has a plugin that transforms all async functions into wrapped generators. This works out really well because generators have been supported in browsers much longer than async functions have.

So, anything that allows me to have await behavior, but doesn't allow me to transform it into the equivalent yield format seems broken to me.

@ljharb
Copy link
Member

ljharb commented Feb 2, 2018

I think that despite how it might be spec'd or implemented, await and yield are very different things.

@jridgewell
Copy link
Member

await and yield are very different things.

Can you explain?

@ljharb
Copy link
Member

ljharb commented Feb 2, 2018

Sorry, hit enter too soon; I mean conceptually very different things. async/await to me is not a generator in any way; a generator is sugar for an iterator - producing multiple synchronous values - whereas an async function is sugar for Promise code - producing one asynchronous value.

(That the default babel transpilation of async/await transpiles to generators isn't necessarily relevant; the wheels are turning to replace that with one that transpiles them to Promises anyways)

@jridgewell
Copy link
Member

a generator is sugar for an iterator - producing multiple synchronous values - whereas an async function is sugar for Promise code - producing one asynchronous value.

The async-to-gen wrapper functions the same way, the wrapper doesn't iterate multiple values. But inside the async function (and the generator in the wrapped form), it is producing multiple sync values. It's just waiting for the promise to resolve before pulling the resuming (or pulling the next value out the generator).

the wheels are turning to replace that with one that transpiles them to Promises anyways

It's unbelievably difficult to implement async-to-promise. I've tried it twice, and failed miserably both times. Exponential code bloat, edge cases that are hard to statically catch. I imagine async-to-gen will be relevant for a long time.

@js-choi
Copy link
Collaborator

js-choi commented Feb 3, 2018

This issue applies only to one of the four current proposals: F#-style/call pipelining. It does not apply to Hack-style/binding pipelining or the two mix proposals. The issue is a thing insofar that yield and yield * might only be weakly desirable goals that may not be worth the cost of holding back F#-style pipes. But they are still at least a little desirable, with zero extra cost in the other three proposals, so it’s a moot problem for those other three.

Maybe the title should be changed to clarify that it applies to the F#-style proposal only? Wrangling all these proposals and their issues is confusing enough; clearly narrowing the scope here may help discussion.

@mAAdhaTTah mAAdhaTTah changed the title Discussion: Should the pipeline support yield? Discussion: Should F#-style pipelines support yield? Feb 3, 2018
@mAAdhaTTah
Copy link
Collaborator Author

Maybe the title should be changed to clarify that it applies to the F#-style proposal only? Wrangling all these proposals and their issues is confusing enough; clearly narrowing the scope here may help discussion.

Took care of this for now. Will share some other thoughts tomorrow.

@mAAdhaTTah
Copy link
Collaborator Author

It's just waiting for the promise to resolve before pulling the resuming (or pulling the next value out the generator).

This is one type of behavior you can model with generators, yield'ing a promise to the wrapper function (e.g. co) and allowing the wrapper to next the value resolved by the promise, but it isn't the only behavior a wrapper function (or any caller of a generator) can implement. Because await conforms to these specific rules about how it interacts with promises and their values, that make it more suitable to positioning itself in a pipeline. It's these rules that make await conceptually different from yield.

But any arbitrary generator wrapper function can implement any given behavior. My inclination is to think this uncontrolled nature of yield makes it less suitable for using in the middle of the pipeline. I think it's unlikely developers will need to pipe through yield such that a wrapper function would want to next into the middle of the pipeline.

However, I think we might be able to optimize yield (and yield *) for the most common case: yield'ing a pipeline result. If we can write it as such:

yield x |> double
  |> add1
  |> square

// desugar to
yield square(add1(double(x)))

I think that would optimize for the most common case.


All of that said, I don't know if it's actually that difficult to support yield the same way we already support await. Even if it's less useful / suitable for the pipeline, it still could be useful, and if it's not hard to include, there's not really a big deal to include it the same way as await.

@littledan
Copy link
Member

As in the initial comment, I don't think yield makes sense, due to the optionality of its argument. yield is already valid as an expression, so it would be odd to give |> yield a different interpretation.

By contrast, await both makes sense and is extremely useful. If we do F#-style pipelining (as opposed to Hack-style), I think we should land #85 to support it.

@zenparsing
Copy link
Member

What is the rationale for creating an ad-hoc solution for await, instead of a general solution for any control flow-type expression?

@zenparsing
Copy link
Member

Oh, I missed the "if" part of that statement. Sorry!

@littledan
Copy link
Member

littledan commented Feb 4, 2018

@zenparsing Well, if something else like await came along which also acted as a pseudo-function which always takes a single argument, we could consider adding it to the group. typeof and import could be candidates for that (cc @domenic). yield just doesn't work.

@mAAdhaTTah
Copy link
Collaborator Author

I'm going to go ahead and close this as "no, it shouldn't". If anyone still objects, we can reopen / revisit this decision.

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