Closed theScottyJam closed 3 years ago
Isn’t that what the current proposal has with a->b()
? It’s only bind without the parens.
It's very similar.
obj->f(2, 3)
is roughly equivalent to f.bind(obj)(2, 3)
while f@(obj, 2, 3)
is roughly equivalent to f.call(obj, 2, 3)
Basically, this syntax is adding a tax when you do a bind without calling the function (see example above), while removing a syntax tax from scenarios such as a->(b.c)()
(you would instead write b.c@(a)
)
Thanks for filing this issue. I appreciate that this f@(o, ...args)
tries to make complex f
expressions more ergonomic. I do think it is important that the o
callee goes first, though, to match regular method calling. But whatever goes.
For what it’s worth, I am considering loosening the precedence of ->
such that a->b.c()
would be a->(b.c)()
(that is, allowing property access on its right-hand side). I first need to figure out if this is grammatically possible, though. I think it should be…
You can’t tell the difference between whether it’s a bind then a call, or just a call - that’s an optimization engines would likely want to do, and one we’d likely want to spec such that it’s required.
Are you saying that as an argument in favor of this call syntax? Because it's really easy for an engine to tell that a bind + call is happening when you do f@()
? While in something like a->b()
it's much harder to tell - I mean, sure, you can analyze the AST on the left of ()
and see a binding is happening, but it gets trickier when you have something like (a->b)()
, or (null, a->b)()
, or { key: a->b }['key']()
, etc.
Or ... are you saying something else?
I'm saying that the existing proposal already doubles as both bind syntax and call syntax, depending on if you immediately invoke it or not. In a->b()
, no binding is happening - just calling with an overridden receiver.
Ah, got it. In the existing proposal, it's obvious when a binding is occurring (the "->" got used), but it's also easy enough to analyze the expression and see that the binding is only getting used for function call purposes, so the engine can skip the binding step and just call the function with the correct implicit "this" parameter.
And while it's pretty obvious in this proposal when a bind-then-call is happening (whenever the @-call syntax is used), there's no simple syntax to perform just a binding - certainly not one that's easily statically analyzable.
Which is why the existing proposal works so well, and precisely solves all the desired use cases (unrelated to how ->
is spelled).
Sorry I'm slow, but just to doubly make sure I understand.
const myFlat = Array.prototype.flat.bind([2, 3])
const myFlat = [2, 3]->(Array.prototype.flat)
const { bind } = Function.prototype
const myFlat = bind@([2, 3], Array.prototype.flat)
We're saying that there's limitations to how an engine can speed up the first line? Is it because it's not statically analyzable (because it's possible the prototype has already mutated before this point)? Or some other reason?
But, once the arrow syntax is introduced, people can write code similar to the second line that's slightly faster?
And lines 3 & 4 don't cut it, because they're also not statically analyzable, similar to the first line, so there's something about it that can't be optimized?
The reason .bind
and .call
can't be relied upon is indeed because they could have been removed before this point, unrelated to performance.
This proposal means the only thing I have to "save" (cache, etc) is the prototype methods I want, instead of also using .call
and .bind
, to manufacture a callBind
function (and both of these must exist on Function.prototype
in order to create this abstraction).
hmm... things just aren't clicking yet.
Here's what I understand if we compare the two proposals performance-wise (ignoring usability and prototype mutation safety)
You mentioned one potential performance optimization that an engine is able to do with the arrow syntax.
In a->b(), no binding is happening - just calling with an overridden receiver.
From what I understand, this specific performance optimization can also happen with the @-call syntax as well. When an engine sees f@(a, b)
, no binding needs to happen, just calling with an overridden receiver.
In fact, as far as I can tell, it should be equally possible to optimize the two syntaxes, except maybe for the cases when you might use ->
just to do a binding without also doing a calling, because the @-call syntax doesn't provide any direct syntax support for this specific scenario. Which is why I was asking in my previous comment whether the different pieces of that example snippets of code would all be equally as fast, or if the ->
would be able to somehow run faster for whatever reason I'm not yet aware of. If the "->" version does run faster than the other parts of that example, then yes, there's a performance difference between these two proposals. Otherwise there's not.
Does this make any sense?
(Perhaps I just got stuck on your performance comment and you moved on from it 🤷♂️️)
Here's what I understand if we compare the two proposals usability-wise, focusing specifically on the use case of protecting against prototype mutations.
I had originally mentioned that the @-call syntax imposed a minor usability tax, because it doesn't provide direct support for bind-only logic, like ->
does. In this scenario, you just have to pick off the "bind" function and @-call it, like you would with any other function you pick off of the prototype. This doesn't feel like a huge loss to me, as it doesn't directly hurt the use case of calling prototypal methods safely, nor is @-calling the bind method that difficult to do (no more difficult than @-calling Array.prototype.map). Of course, you may feel this is much more of a con than I feel it is, and that's totally fine.
I had also argued that the @-call syntax lifted another syntax tax, which is the unfortunate fact that parentheses are required in this scenario: obj->(x.f)()
, but @js-choi informed us that it may not stay that way (fingers crossed). Perhaps there's a way to fix the grammar to allow that to be done without parentheses, but perhaps not. If not, then this @-call syntax might feel a little more attractive.
Even if the grammar can be fixed, I still feel like the two proposals are pretty neck-and-neck. It really just comes down to "Which one feels more intuitive and simpler to use"? (And, how important is the bind-without-call side of the syntax). I argued the @-call syntax is more intuitive earlier, because I found it a little awkward that local variables gets used after the ->
, but I'll also note that I currently don't have a strong preference between the two. @js-choi argued for the ->
syntax because they feel it's important to have the "this" arg on the left-hand side of an operator, but overall didn't seem to have a huge preference. You also seem to prefer the "->" syntax.
That's the pros and cons of the usability side of this syntax, as I can see it.
I think that yours treats the receiver as Just Another Argument, and i would argue that is not very intuitive (albeit how call/bind works now). I also think that “can both call and bind” vs “can only call” is a pretty significant capability difference.
Got it - thanks for sharing your opinions 👍️
Thanks again for filing this issue. I do think I’m not going to pursue this approach, since I think preserving the usual object–method–arguments ordering of ordinary method calling is important, but I appreciate exploring every option. 😄
Ok, I'm going to take another shot at playing with alternative proposal ideas.
This is an idea I've talked about briefly on a different thread. What if, instead of having a syntax shorthand for
Function.bind()
, we had a syntax shorthand forFunction.call
?(I wouldn't be surprised if "@()" creates some conflicts with the decorator proposal. The exact syntax can of course be figured out later)
Why do I like Function.call syntax more than Function.bind syntax?
y
inx->y
is not a property on x, it's a local variable. This is a very big mental shift from how the "->" functions in a language like C++. (This probably isn't a huge deal - I'm sure I would get used to it fairly quickly, but it currently does make me pause for a moment every time I see it)->
syntax is a little cumbersome when the function you wish to bind is found within an object. I think this is a good design choice for->
, but it's also unfortunate because this will likely be a common scenario. CompareArray.prototype.flat@([2, 3])
to[2, 3]->(Array.prototype.flat)()
Note that we're not losing any functionality by using a Function.call() syntax. Here's how we can use the call syntax for a bind-without-call use case.
I view this call syntax as only a marginal improvement over the current proposal, but it still seems like an improvement, at least until others come and expose all of its flaws :).