-
Notifications
You must be signed in to change notification settings - Fork 30
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
Make :: operator a better version of . operator tailored for methods #42
Comments
Variables and functions are resolved by scope, so this can't work well without being confusing. For a real world example, take a look at this:
There are several problems with your proposal that come to light here. The first is the temporal dead zone rule for let/const vars; with
Now we've got a bound version of I agree that the binary form is more intuitive to read, but it can't do the job of the unary form as well as its own job. |
I’m curious about what this example is demonstrating:
... since this yields the same result:
Altogether this seems to involve a lot of complex rules. We’re already in fuzzy territory with a proposal that reuses one token |
@andyearnshaw
Assignment alters the name resolution of RHS for :: in a way that LHS of assignment is always excluded from resolution. Doesn't matter at all if let/const is there. It is done with the intent to fallback to property and is covered with detailed examples by point 4. Global object is also excluded from name resolution as expected in strict mode. Although I could agree that there might be a concern with variable hoisting as I don't know yet how technically feasible is it to discard variable hoisting for name resolution on RHS of :: operator. let boundToString = obj::toString
let toString = function() {} might be interpreted as let boundToString = toString.bind(obj)
let toString = function() {} But the idea is to ignore hoisted variables. Hoisting is not the most intuitive thing either and ReferenceError for let/const is there to actually prevent using hoisted variables. So the expected behavior of let boundToString = obj::toString
let toString = function() {} interpreting as let boundToString = obj.toString.bind(obj)
let toString = function() {} is quite intuitive despite hoisting. |
@bathos let foo = {
bar() {
return this
},
baz() {
return this
}
}
foo
::bar()
::baz() might seem equal to let foo = {
bar() {
return this
},
baz() {
return this
}
}
foo
.bar()
.baz() it actually means let tmp = (foo.bar.bind(foo))()
(tmp.baz.bind(tmp))() or let tmp = (foo.bar.call(foo))
tmp.baz.call(tmp) Could you elaborate on why this can be not analyzable statically? foo
.bar()
.baz() to prevent extra call or bind invocations and creating temporary bound functions. |
I skim-read part 4 of your initial post, I apologise for that.
Your proposed changes are markedly hindering the binary form of the bind operator by introducing odd changes to semantics. Consider:
Let's say, for argument's sake, that this piece of code is inside the scope of another function on line 2421 of foo.js. Later, we decide to import something from another file and we've forgotten all about the other piece of code:
Your proposal doesn't make it completely clear what happens here. Does the imported The current proposal doesn't make any changes to identifier resolution, it just takes advantage of it in a really nice way. The way the binary form works right now is pretty much perfect IMO, and we should leave it as it is. |
@andyearnshaw
And now imagine you have 10 of those unary statements in one block of code. This change significantly reduces that mental burden. You always know that RHS is a function which is executed with LHS as this. LHS is obvious and the only effort you might need to do is figure out what RHS means if it's not obvious by name. Always using And I do agree that shadowing is a refactoring hazard. But it's rather a corner case than intended use case. And static analyzers could mitigate that by indicating a smell and/or a warning just as they do with variable shadowing currently.
I wouldn't call the resulting semantics odd. Consider the extension methods in C#.
Sorry, my bad.
or
or
Fixed. Of course hoisting itself is counter intuitive. It was designed for the convenience of interpreter developers and not for mental sanity of application developers. Sorry again for poor wording. Btw, if with current proposal we would have the following code foo()
::bar.baz Would you read it as an unary or a binary form? How long does it take to figure out? |
Yes. This distinction is entirely ‘internal’ to the expression though. The
Yeah. Consider the following module:
With the bind operator defined as it is in the current proposal,
But in your proposal, that cannot be detected as a reference error. By removing Static analysis aside, that’s clearly a footgun. By removing the declaration of Also worth noting: the distinction between property literals and binding identifiers is done at the parsing level. Your proposal would require these two productions to be conflated and resolved at runtime. However, they actually have different rules, because property literals may be reserved words and binding identifiers may not. What happens here?
|
@bathos Yes, if we would only have a binary form as in current proposal it would be more straightforward than this suggestion. But we also have an unary form in proposal. And together they are a mess.
No, that's not completely true. In your simple example yes. //bar.js
const bar = {
baz() { return this * 5; }
};
//otherfile.js
delete bar.baz;
//myfile.js
const foo = 2;
foo::bar.baz(); Reference error? Yes. Involves property lookup? Yes. Statically analyzable? No. Runtime error. let mapping = arrayLike::Array.prototype.map(mapper) I do agree that implicit fallback to properties is a footgun. But it's not a bigger footgun than the unary form. If you speak in OOP terms you could say that RHS is always a method to be executed in LHS context,
No. In module scope it is totally possible to resolve it statically. Runtime resolution would only be needed when code belongs to global scope.
const foo = { default: function() {} };
foo::default(); As per my version it's simple. I will rephrase the initial post to shift the emphasis to intended use cases and outline gotchas found during discussion. |
If it’s needed somewhere, is it not still needed? It isn’t the resolution of the reference that I was talking about there, but the determination of whether the token is an
Sure it is. In the unary form, the property being absent is just our good old friend, Altogether, while you’ve proposed solutions for lots of the hairy aspects of this, I’m afraid it ends up sounding like a cluster of too many exceptional cases (it does foo unless bar except when baz...). I would suggest that a viable proposal:
I would point to this comment, where it was explained that the pushback from TC39 to date has partly been due to concern about making property access more confusing to people. Since this adds a new concept of "conditional" property access — almost like the deprecated |
Then, actually, we do agree. I agree with your reasons for why the unary form isn't great. I just don't agree that butchering the binary form is the correct way to fix the problem. In all honesty, I'd rather see the unary form held back and just keep the binary form. |
@andyearnshaw function baz(){}
//extension
foo
::bar()
::@baz()
::bang()
//extraction
const boundBar = foo::bar
//binding
const boundBaz = foo::@baz is still quite expressive and clearly states all the intents. |
Highly informative past discussion on this: #26. Note that it's a year old, but it should be a great read. |
I will close this in favor of a more fine grained opinion.
Examples of interpreting the pipelining
(piped => piped.bang.call(piped, e, f))(
(piped => piped.baz.call(piped, c, d))(
foo.bar.call(foo, a, b)
)
) or with little help from lodash _.flow(
piped => piped.bar.call(piped, a, b),
piped => piped.baz.call(piped, c, d),
piped => piped.ban.call(piped, e, f)
)(foo) |
The idea is to extend semantics of binary
::
operator to do a bind operation along with a scope resolution operation for methods and replace.
operator for method access. The resolution algorithm will differ from regular property resolution to cover use cases for both binary and unary form of::
in current proposal but also add value on top of that.There are technical challenges and gotchas in certain corner cases of this approach but a benefit from reduction in cognitive load compared to current proposal should outweigh them.
Term
method
here is supposed to stand for a function determined by RHS to be executed in context of LHS object.The following kinds of methods can be outlined:
In short
::
operator can be described as method access operator able to capture extension methods from lexical scope.The rationale
::
operator will be a "this-safe" version of scope resolution operator unlike.
.::
operator will always show the intent of having a bound method.::
instead of.
will visually separate method access with intent of invocation from "method as property" access.::
will always mean method it would be possible to verify that RHS is invokable before the actual execution of bound function occurs.Use cases
The following use cases can be defined
RHS resolution algorithm
global
object=
is excluded from resolutionConcerns
Refactoring hazard when newly introduced variable in scope shadows object method.
In module scope this can be statically analyzed and produce a warning. In global scope it gets unpredictable.
When in global scope RHS identifier resolution requires runtime checks. In module scope it can be resolved statically.
Examples
Method binding
interprets as
Method adoption
interprets as
LHS of
=
exluded from resolutioninterprets as
Same with destructuring
interprets as
Nested assignments
interprets as
Ignoring hoisted variables
interprets as
Property shadowing
interprets as
The text was updated successfully, but these errors were encountered: