tc39 / proposal-bind-operator

This-Binding Syntax for ECMAScript
1.75k stars 30 forks source link

::object.func !== ::object.func #17

Closed lukescott closed 8 years ago

lukescott commented 9 years ago

I'm concerned that this syntax can be very misleading. For example:

var object = {
  clickHandler() {}
}

element.addEventListener("click", ::object.clickHandler);
// later in the code...
element.removeEventListener("click", ::object.clickHandler); // doesn't work!

console.log(::object.clickHandler === ::object.clickHandler); // false

It's not immediately clear the two are not the same thing - something new is being created each time, which is being masked by the hidden bind.

dead-claudia commented 8 years ago

I would actually be inclined to disagree with the idea ::obj.foo should be identical to ::obj.foo. Although I see potential footgun material, what good is it supposed to do? Not to mention it would require significant boilerplate to polyfill, and a lot of reference counting. It would hardly improve speed, as it is already very cheap and easy to create a fast path in engines for. Allocate a closure with two pointers (host and function), and set the [[Call]] and [[Construct]] behavior to directly call the methods with little memory or speed overhead.

As for memoization, it's not much of a speed or space optimization:

  1. It would actually increase space, because not only the typical reference counting, you now have an unordered weak map for any object with a bound method. This would use more pointers than any constructed closure.
  2. It would be slower to create and recall. You have to include extra logic to ensure that the new closure isn't recreated. And testing for existence in a weak collection isn't as quick as a simple key-value map.
  3. Because the common case is throwaway methods, it would end up having to construct new instances more often than not. As a weak collection, the throwaway method would be recycled early in most GCs. And adding/removing an instance from a weak collection is slower than constructing a bound method with a few smart pointers.

(Yes, I have a clue how engines implement JS. I've read most of the V8 code base, and large sections of SpiderMonkey. I also have a clue how several other languages' runtimes are implemented.)

And now for the other reason: if it's memoized, then it doesn't maintain the usual invariant that functions are never identical. That will become another footgun, although less common. And also, where ::obj.method is currently equivalent to obj.method.bind(obj), would one expect that obj.method.bind(obj) === obj.method.bind(obj)?


And as for discussion on a related proposal, I will kindly refer you to #26 (which is still somewhat active).

WebReflection commented 8 years ago

I see potential footgun material

potential VS common use cases

because the common case is throwaway methods

that's a side effect of what developers think bind does, not their intention in most of the cases.

Developers do believe if they add obj.method.bind(obj) as listener, as example, they can remove it via obj.method.bind(obj) which is the number 1 footgun about bind.

Moreover, please tell me a single use case for having obj.method.bind(obj) being different from obj.method.bind(obj) ... because AFAIK nobody needs that and most developers think already that's how it is, forgetting to store references, unable to "regret" and update their state.

Last, but not least, :: is fresh new syntax, the same it was for => fat arrow ... there was nothing like fat arrow before, that doesn't mean fat arrow is bad or anything, that simply means fat arrow is born more mature and thoughtful ... and so should be ::obj.foo proposal.

dead-claudia commented 8 years ago

I'm more speaking from implementation standpoint. And I see massive memory leak potential with that semantic. That's my main concern.

On Tue, Dec 8, 2015, 21:22 Andrea Giammarchi notifications@github.com wrote:

I see potential footgun material potential VS common use cases

because the common case is throwaway methods that's a side effect of what developers think bind does, not their intention in most of the case.

Developers do believe if they add obj.method.bind(obj) as listener, as example, they can remove it via obj.method.bind(obj) which is the number 1 footgun about bind.

Moreover, please tell me a single use case for having obj.method.bind(obj) being different from obj.method.bind(obj) ... because AFAIK nobody needs that and most developers think already that's how it is, forgetting to store references, unable to "regret" and update their state.

Last, but not least, :: is fresh new syntax, the same it was for => fat arrow ... there was nothing like fat arrow before, that doesn't mean fat arrow is bad or anything, that simply means fat arrow is born more mature and thoughtful ... and so should be ::obj.foo proposal.

— Reply to this email directly or view it on GitHub https://github.com/zenparsing/es-function-bind/issues/17#issuecomment-163085959 .

zenparsing commented 8 years ago

I think @WebReflection 's argument regarding addEventListener/removeEventListener is convincing, however as we've explored above securely memoizing the extracted method seems like it will be rather heavy-weight.

Also, it creates a divergence from the current behavior of "bind"; it means that this proposal becomes something more than just syntactic sugar. There's certainly nothing wrong with adding core capabilities to the language, but the current proposal just "slides right in" to the existing object model. I find that to be really attractive.

Additionally, it would create a semantic divergence between the unary form and the binary form of the operator, which we would prefer to avoid.

In response to the addEventListener/removeEventListener issue, in my opinion I would like JS/HTML to continue moving toward better async APIs which don't require callback registration/de-registration patterns. For consuming UI event streams, I like Observables.

dead-claudia commented 8 years ago

@WebReflection @zenparsing That's a good point as well. My big worry about memory leaks are this:

// obj.js
import {EventEmitter} from "events"
export const obj = {a() { return this }}
export const ee = new EventEmitter()
// foo.js
import assert from "assert"
import {ee, obj} from "./obj.js"
let foo = ::obj.a
ee.on("ev", function listener(f) {
  assert.strictEqual(f, foo)
  foo = null
  ee.removeListener("ev", listener)
})
// bar.js
import {ee, obj} from "./obj.js"
ee.emit("ev", ::obj.a)
// main.js
import "./foo.js"
import "./bar.js"

My question: does a reference to ::obj.a exist in main.js after the imports? Logically, it shouldn't, but this is actually a very complex problem to algorithmically solve, and in fact, this could result in memory leaks if the GC fails to realize ::obj.a is no longer accessible.

zenparsing commented 8 years ago

I think we've exhausted this thread, thanks everyone. If anyone wants to continue discussing any of these points, feel free to open a new issue.