tc39 / proposal-bind-operator

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

Can't we just have a simple object::method binding syntax? #54

Open icholy opened 6 years ago

icholy commented 6 years ago

Why are you trying to make a simple thing complicated? All we need is sugar to:

// convert this
this.service.method.bind(this.service)

// into this
this.service::method

Drop the pipeline non-sense.

ljharb commented 6 years ago

They achieve different purposes; the bind operator has 3 use cases, 2 of which pipeline solves - method extraction is the one it doesn't solve, which will need to be addressed somehow.

WebReflection commented 6 years ago

obj::method === obj::method is also an unsolved need of the community, that keeps adding listeners via bind or, in the future, pipe operator.

If only obj::method could translate into the following bound(obj, method) that'd be awesome.

const wm = new WeakMap;
const create = obj => {
  const map = new Map;
  wm.set(obj, map);
  return map;
};
const set = (obj, method) => {
  const map = wm.get(obj) || create(obj);
  const bound = method.bind(obj);
  map.set(method, bound);
  return bound;
};
const bound = (obj, method) => {
  const map = wm.get(obj);
  return map && map.get(method) || set(obj, method);
};

So that bound(obj, method) === bound(obj, method) would be true.

zenparsing commented 6 years ago

@WebReflection I agree we need to address this pain point.

Syntactically speaking, binary :: probably won't work, since we will likely want to support method extraction for computed property names. We might be able to come up with a reasonable prefix operator though.

tiansh commented 6 years ago

Nothing different here with following python code:

class A:
    def f(): pass

a = A()
print(a.f is a.f) # False

I don't think there are any importance for obj::method === obj::method.

WebReflection commented 6 years ago

@tiansh I guess that's because you've never added a listener you want remove later on

obj.addEventListener('type', obj::method);

That is why is important, many developers write obj.addEventListener('type', obj.method.bind(obj)); thinking they can remove that later on.

I've solved this via {handleEvent()} trap but not everyone knows it and it doesn't work in NodeJS Emitter.


About the pipeline VS method extraction, I've created an utility that does something similar to this:

Function.prototype.this = function () {
  return self => this.apply(self, arguments);
};

That plays super nicely with pipeline operator:

const {map, sort} = Array.prototype;
const names = document.querySelectorAll('*')
                |> map.this(el => el.nodeName)
                |> sort.this();

If that could be proposed in core, it'd be awesome.

tiansh commented 6 years ago

@WebReflection What you need is just saving it to a variable for further reference. Nothing special here. And that is the most understandable way. Nothing magic here. It at least follow the traditional of this language. And do not enviove too many things to make it complex, which should also be new learner friendly.

WebReflection commented 6 years ago

You really don't have to explain me anything, developers keep doing that mistake, not me, never me. I use handleEvent or cached listeners already.

tiansh commented 6 years ago

Yes, that’s because bad api design. Not by language. And this (use a uncatched right value for removeEventListener) can be easily detected by browsers and linters. But not by language.

zenparsing commented 6 years ago

@tiansh In python, bound methods do not have object identity but they do have equality, which makes a difference:

class A:
  def f(): pass

a = A()
d = dict([(a.f, 1)])
a.f in d # True
ExE-Boss commented 5 years ago

This would be similar to the Java 8 method reference syntax, which I consider a plus:

Consumer<String> println = System.out::println;
println.accept("Some string");

// Equivalent to: (Anonymous object implementation)
var println = new Consumer<>() {
    public void accept(String string) {
        System.out.println(string);
    }
};

// And to: (Java 8 lambda syntax)
Consumer<String> println = string -> System.out.println(string);

Uses the Consumer<T> functional interface as an example.

icholy commented 5 years ago

Having obj::method === obj::method doesn't seem very difficult.

const sym = Symbol("extracted");

Object.prototype.extract = function (method) {
    if (!this[sym]) {
        this[sym] = new WeakMap();
    }
    if (!this[sym][method]) {
        this[sym][method] = method.bind(this);
    }
    return this[sym][method];
};
ljharb commented 5 years ago

Nothing new can be added to Object.prototype; and that wouldn’t address null objects anyways.

icholy commented 5 years ago

@ljharb this method is supposed to represent the :: operator's behaviour. I'm not suggesting actually adding an extract method.

ljharb commented 5 years ago

in that case, storing the cache observably and mutably on the object - which might be frozen anyways - is a nonstarter.

WebReflection commented 5 years ago

the solution for equality was already posted here and it's still not difficult: https://github.com/tc39/proposal-bind-operator/issues/54#issuecomment-385111531

this proposal is going nowhere anyway, so I think it could be closed?

Fenzland commented 4 years ago

That's hitting the real pain point. Binding a method from third part library can be substituted by |>. What we really need is to bind methods from prototype of the object itself.

There is a plan to mix them together.

function addClass( className ){ this.classList.add( className ); }

const foo= document.getElementById( 'foo' );

const setAttributeToFoo= foo::setAttribute;
const removeAttributeFromFoo= foo::['removeAttribute'];
const addClassToFoo= foo::{addClass};