Skip to content

How is the receiver treated? #1

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

Closed
littledan opened this issue Jul 20, 2017 · 14 comments
Closed

How is the receiver treated? #1

littledan opened this issue Jul 20, 2017 · 14 comments

Comments

@littledan
Copy link
Member

In something like

x.y.z(?)

A couple questions:

  • Does this just look up the function at x.y.z and call it with the argument, or does it call with x.y as the receiver?
  • Is x.y evaluated each time, or just once at the beginning? How about the GetMethod of z?
  • How about if it's a longer chain, like a[x].b().c.d().y.z(?)--where does the chain cut off exactly? Should this somehow correspond with optional chaining's extent?

I'm asking because @rbuckton and I have talked about alternatives here, and I don't see the answer explicitly mentioned in the document (though I guess the .call() examples would not work if the receiver is not preserved somewhat).

@rbuckton
Copy link
Collaborator

rbuckton commented Jul 20, 2017

The goal is to preserve the receiver, but cause side effects. A complete syntactic conversion of x.y.z(?) might be something like: (NOTE: See #1 (comment))

var _temp;
(_temp = x.y, (a, ...b) => _temp.z(a, ...b));

@rbuckton
Copy link
Collaborator

I'll add this example to the strawman.

@littledan
Copy link
Member Author

I like these semantics!

@rbuckton
Copy link
Collaborator

Hmm. This does beg the question as to whether we should eagerly evaluate all fixed arguments as well, or lazily evaluate. The strawman indicates lazy evaluation which preserves side effects. As such, my example above is inconsistent. The more consistent approach would be to have x.y.z(?) become a => x.y.z(a) and not cache the receiver.

@littledan
Copy link
Member Author

That's a legitimate argument, though my intuition is more that the receiver is different, e.g., it can't be substituted with ?, it's not in the parens, ...

Nit-pick: I'd prefer avoiding saying "lazy evaluation" here--that often means evaluating something once and caching it. Here, it's evaluating it separately each time

@rbuckton
Copy link
Collaborator

True. I don't intend to use the term 'lazy' in the strawman.

@rbuckton
Copy link
Collaborator

Generally o.f(?) is synonymous with _ => o.f(_). As such, it preserves the same runtime semantics found in arrow functions and function expressions. Changing that behavior for this case seems like a possible source of confusion. I'd rather remain consistent.

@littledan
Copy link
Member Author

Well, the question then becomes, how far back do you go to capture the calculation of the receiver? What is a[x].b().c.d().y.z(?)? I think the easiest option would be to say, just go back one method call, or for a function call--evaluate whatever led to getting the function once, keep the receiver around, and return the function.

I don't think it's reasonable to make the mental model for all new features be a trivial syntactic translation to old features. Sometimes, it can make things worse (here, due to the scope ambiguity issue).

@rbuckton
Copy link
Collaborator

I think the syntactic conversion is besides the point. However, this is a good question to pose to the committee: Do we preserve existing runtime semantics to be consistent with the rest of the language, or is it better to follow what might be the common intuition that the receiver should be fixed once and not reevaluated? I'm erring on the side of the former for now.

@Jamesernator
Copy link

Regarding the question of whether other arguments should be eagerly evaluated, it seems like either direction is going to be surprising e.g.:

// With eager this would probably do what you expect
// Create a factory for { fizz() {} } objects
const factory = Object.create({ fizz() {} }, ?)

// With eager this probably wouldn't do what you expect
// namely sharing the same reference causing the same object
// to be many places on the tree
const addLeft = Reflect.set(?/*tree*/, 'left', { left: null, right: null })

I prefer the "lazy" form myself as sharing potentially mutable references generally doesn't seem a great idea to me. I also don't think it's even possible to simulate the "lazy" form using the eager form, whereas the converse is always possible just by supplying a reference:

// With "lazy" you can always do this:
const proto = { fizz() {} }
const factory = Object.create(proto, ?)

// No such analog for eager as you can't make an expression become lazy
// after the fact

You could even in theory have some syntax for coercing the arguments to be evaluated at definition time (or you could make it eager by default but use the operator to defer to call time) e.g.:

// Using % just for exposition
const factory = Object.create(%{ fizz() {} }, ?)

function div(a, b) {
    if (b === 0) {
        throw new Error("Division by zero!")
    }
    return a/b
}
// Using such an operator would throw at creation time
const add5 = add(%div(1, 0), ?) // Throws immediately not at call time

// ------
// Or conversely if it was eager by default:
// the syntax would be used to say I want this lazily evaluated each call
const addLeft = Reflect.set(?, 'left', %{ left: null, right: null })

@rbuckton
Copy link
Collaborator

I just thought of another use case that may be worth adding to the README:

class Control extends HTMLElement {
  constructor() {
    super();
    this.onclick = this.clicked(?);
  }
  clicked(e) {
  }
}

@littledan
Copy link
Member Author

Do we preserve existing runtime semantics to be consistent with the rest of the language, or is it better to follow what might be the common intuition that the receiver should be fixed once and not reevaluated?

I agree that consistency is a good goal in decisions like this.

I don't see how one or the other of the options here is more consistent with the rest of the language. Could you explain more? Evaluating an expression multiple times that might look like it's evaluated just once, syntactically, could be considered less consistent with the rest of the language; that's my intuition here, personally.

@vadzim
Copy link

vadzim commented Jan 24, 2018

I think it could be useful first to decide using of which thing we want to make more comfortable - either arrow functions or .bind function.
IMHO arrow functions are already expressive enough and they do not need other improvements.
On the other hand .bind is too limited - it allows to fix only arguments at the beginning.
So I believe it'd be better to implement call by value semantics.

@vadzim
Copy link

vadzim commented Jan 24, 2018

If someone looks at a function, either regular or arrow or generator or async, he understands that this piece of code have to be executed at some other moment, not right now. So functions declare code, but do not evaluate it.
But if he sees any other language construction then he believes that this expression is going to be evaluated right at that line of code. This includes calling just declared function.
It seems it's more consistent to leave side effects for functions and do not introduce other syntax constructions with side effects.

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

No branches or pull requests

4 participants