function.sent beta equivalence?

# Jason Orendorff (9 years ago)

Quick question about function.sent:

As of ES6, there's this parallel between functions and generators:

// how to wrap a function
// f2 is equivalent to f, if f doesn't use `this`.
function f(...) { ... }
function f2(...args) { return f(...args); }

// how to wrap a generator
// g2 is equivalent to g, if g doesn't use `this`.
function* g(...) { ... }
function* g2(...args) { return yield* g(...args); }

That is, λx.f(x) = f, a sort of beta-equivalence rule, and for generators, you just need to add yield* to get an analogous rule. My understanding is that this is one reason we have yield*. (Writing a wrapping function/generator that also covers this is left as an easy exercise.)

Given function.sent, is there still a way to "wrap" a generator?

# Brendan Eich (9 years ago)

Hrm, the meeting notes don't quite capture it (tc39/tc39-notes/blob/bf5190257d22d1c700e203b28edc8d85ee3aadcc/es6/2015-05/may-27.md is one view; my fault for not editing after), but we have discussed passing function.sent into the sub-generator in the past. Ben Newman first raised the idea the other year, but we finally all came to see at the last (May) meeting that there's no good way to do it in general.

When you write a delegating generator that does nothing else, it's tempting to want function.sent delegation. But in general the yield* could want a different first-next param than what was passed down, especially when the delegator looks like this:

function *g2(...args) { prolog code here; maybe-return yield* g(...args); optional epilog here }

See the discussion from the May meeting, I'll cite it here:

MM: But it's passing something from before when yield * is evaluated.

DH: I'm beginning to think that this is the wrong path (passing in function.next via yield *).

AWB: You could create some kind of wrapper if you wanted to pass in the first value to the subgenerator.

MM: Libraries could do this.

yield  *  wrap(g,function.next);

MM: "wrap" returns an iterator which wraps the generator which primes the subgenerator with the supplied value.

Does this make sense? Sorry again for sparse notes.

# Mark S. Miller (9 years ago)
# Jason Orendorff (9 years ago)

On Wed, Jun 24, 2015 at 2:17 PM, Brendan Eich <brendan at mozilla.org> wrote:

MM: Libraries could do this.

yield * wrap(g,function.next);

MM: "wrap" returns an iterator which wraps the generator which primes the subgenerator with the supplied value.

Thanks for finding this discussion. This is exactly what I'm interested in. What does "wrap" look like? It seems like it would be pretty involved.

# Mark S. Miller (9 years ago)

Something strange and bizarre that Jafar and I just discovered after the TC39 meeting:

We all know that there are a set of conventional combinators, like flatMap, that work fine for iterators.

Iterators are uni-directional. Generators are bi-directional. (As Jara observes: Generator implements Iterator, Observer)

We thought that these combinators did not apply to generators in their full bidirectional glory. We were wrong.

Flatmap of generators can approximately be modeled in terms of "yield* wrap" as follows:

flatMap([g1, g2])

is equivalent to

(function*() {
  yield* wrap(g1, function.sent); // result of g1 ignored!
  return yield* wrap(g2, function.sent);
}())

On Wed, Jun 24, 2015 at 3:04 PM, Jason Orendorff <jason.orendorff at gmail.com> wrote:

Thanks for finding this discussion. This is exactly what I'm interested in. What does "wrap" look like? It seems like it would be pretty involved.

Just showing the next method:

function wrap(g, prime) {
  let first = true;
  return {
    next(val) {
      if (first) {
        first = false;
        return g.next(prime);
      } else {
        return g.next(val);
      }
    }
    //...
  };
}

ALL CODE I WROTE ABOVE IS UNTESTED.

Does this make sense? If anyone tests it, does it work?

# Mark S. Miller (9 years ago)

On Wed, Jun 24, 2015 at 3:17 PM, Mark S. Miller <erights at google.com> wrote:

flatMap([g1, g2]) is equivalent to

(function*() {
  yield* wrap(g1, function.sent); // result of g1 ignored!
  return yield* wrap(g2, function.sent);

I think this second yield* should just be

  return yield* g2;
# Brendan Eich (9 years ago)

Jason Orendorff wrote:

What does "wrap" look like? It seems like it would be pretty involved.

It might be the full, hideous, but write-once complexity of the desugaring of yield*, but now I'm seeing fragments and missing-uncited-text messages from Mark, and wondering whether I'm missing a reply. Will wait a bit.

# Mark S. Miller (9 years ago)

I only sent three messages in this thread before this one. The first was completely blank, which was only a premature hitting of send, so only two real messages.

# Bergi (9 years ago)

Mark S. Miller schrieb:

Flatmap of generators can approximately be modeled in terms of "yield* wrap"

So you have a signature of Iterable<Generator<i, o, r>> -> Generator<i, o, r> in mind, not a monadic join-like function that takes a generator?

flatMap([g1, g2]) is equivalent to

Maybe it should be

(function*() {
    return [yield* wrap(g1, function.sent), yield* g2];
}())

with Iterable<Generator<i, o, r>> -> Generator<i, o, Iterable<r>>?

# Jason Orendorff (9 years ago)

On Wed, Jun 24, 2015 at 3:17 PM, Mark S. Miller <erights at google.com> wrote:

Just showing the next method:

Oh, I see. We can use an intermediate wrapper object here that's not a generator.

OK, I don't imagine people doing this in the case where they're using generators to implement asynchronous processes. I suppose they just won't use function.next in that case. Which is fine.

Still, it's funny that at the language level we can't have an "elimination form" in yield* f(...) that's directly dual with the "introduction form" of function*(...){...}. Is there some PLT-ish explanation for the asymmetry? Just hoping to understand a little better the relationship between functions and generators.

I'm not sure it'll be clear what I mean by "relationship". I mean a mathematical sort of relationship. If you think of the statements and operators of JavaScript as combinators in some kind of algebra on computations, both functions and generators are built up mostly from the same combinators, and they have analogous behavior. That is, they follow the same rules of execution: those rules are only specified once, not twice. The differences are around invocation, but even there, though you have to use slightly different syntax, some analogies hold (as in my original post on this thread).

My sense is that function.next is outside this relationship, and it shows as a bit of a wart every time you write abstract code about generators. For example, Mark's flatMap on generators must pass undefined as the first value to g2.

# Brendan Eich (9 years ago)

Jason Orendorff wrote:

My sense is that function.next

s/.next/.sent/

is outside this relationship, and it shows as a bit of a wart every time you write abstract code about generators.

Yes, agree there's a smell. We did not automate delegating function.sent for good reason, though. Is there another way to attack the problem than by looking for an implicit propagation in the elimination form?

For example, Mark's flatMap on generators must pass undefined as the first value to g2.

Yup, smell -- not the worst, maybe not even offensive, but definitely an aroma!

# Andreas Rossberg (9 years ago)

On 24 June 2015 at 22:56, Jason Orendorff <jason.orendorff at gmail.com> wrote:

That is, λx.f(x) = f, a sort of beta-equivalence rule,

Nitpick: this equivalence is eta, not beta. Unfortunately, I don't have anything more profound to add.

But it's a good question. Maybe it's an indication that we should not add function.sent?

# Brendan Eich (9 years ago)

Andreas Rossberg wrote:

But it's a good question. Maybe it's an indication that we should not add function.sent?

We talked about this at the last meeting, though maybe you weren't there (meeting notes header with people present and their 2 or 3LAs gets copied forward a lot!). It doesn't seem like there is an equivalence in general, only in the pure delegation case. Does that not strongly suggest that something explicit needs to be said in that or any other case to delegate the first-next parameter?