tc39 / proposal-bind-operator

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

::new collision? #29

Closed bmeck closed 8 years ago

bmeck commented 8 years ago

Isn't

class Foo {
  static new() {
    console.log(123);
  }
}
Foo.new();
new Foo();

ambiguous with regard to Foo::new, shouldn't it be something more like new::Foo?

zenparsing commented 8 years ago

Thanks for bringing this up; I was hoping we could get some discussion going!

Remember that the binary form never looks up properties on the RHS:

obj::blahblah;  // Bind 'obj' to function 'blahblah'

In the above, we don't look up the "blahblah" property of "obj".

The best way to think of obj::new is to regard it as sugar for this:

obj::function(...args) { return new this(...args) }

(Currently, it's not implemented quite that way, but it's close enough.)

So the RHS can define a "new" property and there's no conflict.

class C {
    constructor(a) {
        this.a = a;
    }
    static new {
        return "Hello";
    }
}

console.log(C.new(1)); // "Hello"
console.log(C::new(1)); // { a: 1 }
bmeck commented 8 years ago

idk this is very confusing to me since new is not a function, would this mean typeof, and void would also be valid to bind?

bmeck commented 8 years ago

Also, does this mean to bind static new() {...} you would use $this::C.new(1) ?

domenic commented 8 years ago

Yes, I think this C::new syntax is not going to make it personally, and am choosing not to worry about it. It's just too confusing; it's tacking a new proposal onto this one and reusing the same syntax.

WebReflection commented 8 years ago

Agreed with Domenic, I personally wouldn't create a precedent where a generic property like new should be reserved for the :: syntax.

We passed that phase on ES5 already, allowing obj.class, obj.new and all others, it'd be a bad and (IMO) unnecessary precedent/exception for :: where exceptions rarely indicate a good design choice.

Just my 2 cents

On Mon, Dec 14, 2015 at 5:11 PM, Domenic Denicola notifications@github.com wrote:

Yes, I think this C::new syntax is not going to make it personally, and am choosing not to worry about it. It's just too confusing; it's tacking a new proposal onto this one and reusing the same syntax.

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

bergus commented 8 years ago

Uh, I was shortly confused by this now, but actually it sorts out quite simple.

Methods that are named new can still be bound using the unary operator:

::Foo.new

while only the binary form is attaching a special behavior to the new keyword that would otherwise not even be allowed in this position - it's not a valid identier (variable name).

Foo::x   // x.bind(Foo)
Foo::new // new.bind(Foo) - invalid anyway, therefore we desugar that to
         // (...args) => new Foo(...args)
Artazor commented 8 years ago

For me Lhs::new looks ok, since the new itself is not a valid expression when used alone.

@bergus, exactly!

bergus commented 8 years ago

Maybe we should flip the operands and use

new::Foo

instead. Imo this is better because the function that is evenutally going to be called is always on the right side. And there's less confusion, nobody can mistake this as a property access on new.

The only disadvantage could be that it makes the grammar a bit more complicated, as we must prevent this from parsing as new (::Foo…).

WebReflection commented 8 years ago

you are both expecting a special meaning for a name ... try for a second to think new is the exact same thing as whatever and rethink your examples? That's all I was trying to say.

:: is not for classes only, it's a generic syntax sugar, right?

Artazor commented 8 years ago

However, I agree that this is completely different proposal. Can we reuse otherwise invalid form of the current proposal - that is the question.

zenparsing commented 8 years ago

@bergus Right, new::Foo would require a lookahead restriction within the MemberExpression production.

While technically possible, I don't think it makes sense. You're not binding something to the constructor. You're binding the constructor to a "generic newer method", i.e.

function genericNewer(...args) {
    return new this(...args);
}

const fooNewer = genericNewer.bind(Foo);

Instead of that, we can simple write:

const fooNewer = Foo::new;

@WebReflection Yes, :: is sugar for binding an object to a function.

bmeck commented 8 years ago

@zenparsing but new, typeof, void etc. are not functions, it should be a different proposal.

zenparsing commented 8 years ago

@bmeck Keywords are re-used in various places, e.g. new.target. The fact that new is not actually an object doesn't mean that we can't reuse the keyword in very targeted ways.

The benefit of Foo::new is that it removes pressure on classes to make them somehow by default callable factories (which is a common request).

bmeck commented 8 years ago

@zenparsing meta-properties are very explicit (the fact that new cannot be an identifier), while I don't feel that this syntax is clear since it is on the right hand side of an operator with similar feeling to the . operator.

bmeck commented 8 years ago

also if ::new lands I would be much more comfortable if all unary operators landed as it would not be a special snowflake.

zenparsing commented 8 years ago

@bmeck I can tell that you don't like it, but why exactly? Is it because that you feel it confuses property lookup with binding? If so, then why do you think that's not a problem for the entire proposal? In other words, why is:

Foo::new;

A problem for you, but,

import { something } from "somewhere";
foo::something();

not?

bmeck commented 8 years ago

@zenparsing something is:

  1. a valid identifier
  2. a function

new is neither. Treating it as the same syntax is confusing when I see 2 major differences.

It also is confusing since typeof and void which are in the same category of named unary operators are not talked about.

bmeck commented 8 years ago

Having a syntax with an operator that mimics a binary operator : lhs operator rhs just adds to the above since it makes new a right hand side value only for this operator.

zenparsing commented 8 years ago

@bmeck But the point is that "something" could equally be confused with property lookup as new. I'm not understanding how Foo::new creates any additional hazard in that regard.

Having a syntax with an operator that mimics a binary operator : lhs operator rhs just adds to the above since it makes new a right hand side value only for this operator.

Again, the same exact argument could be applied to new.target, with the keyword on the LHS.

True, this is a special-case overload of the new operator, and no-one particularly likes special-cases. Special-cases have to be sufficiently justified, and you could argue that the justification isn't adequate.

Artazor commented 8 years ago

@WebReflection, you are right.... it is true.

Now I'm started to think that this proposal can be abandoned in favor of plain ES6 helper function

function NEW(...args) { 
    return new this(...args)
}

Or the hypothetical TypeScript (with variadic generics or variadic kinds, and thisArg typing)

function NEW<T, ...Args>{new:(...args:...Args):T}::(...args: ...Args): T {
    return new this(...args);
}

Thus, A::new is effectively modelled with A::NEW

Um?

zenparsing commented 8 years ago

@Artazor Yeah, possibly it would be better to leave it to user space. If it's common enough, then syntax sugar makes sense (so that user's don't have to constantly import that NEW function).

Either way, I think I'd like to keep in the new lookahead restriction just in case.

bmeck commented 8 years ago

@zenparsing meta-properties are different for a few reasons:

  1. They are on the LHS of an operator, as such they can form their own basis of authority. RHS affecting how an operator functions is not present in JS, however LHS has precedent with property descriptors.
  2. They expose similar rules as their non-meta counterpart (no special effects occur by using specific aspects of the properties [::new produces a wrapper that other :: uses do not]).
zenparsing commented 8 years ago

RHS affecting how an operator functions is not present in JS

But new on the right doesn't really affect the semantics of the operator. The new keyword is just shorthand for the "generic newer method" in that context.

::new produces a wrapper that other :: uses do not

Bound functions are all wrappers. Foo::new produces a wrapper just like the other forms.

So I'm just not understanding the arguments.

I think we'll have to agree to disagree for now. Let's let this settle for a few days and see how it looks. Thanks for all the feedback though!

bmeck commented 8 years ago

@zenparsing agree, not saying it can't go in, but staunchly against it going in the same proposal. Will get back to this on Thursday

bergus commented 8 years ago

@zenparsing

While technically possible, I don't think it makes sense. You're not binding something to the constructor. You're binding the constructor to a "generic newer method", i.e.

function genericNewer(...args) {
    return new this(...args);
}
const fooNewer = genericNewer.bind(Foo);

I think you got your statements backwards - you are binding that generic newer method to the constructor, like Foo::genericeNewer would do - but I get what you are saying. Foo::new makes sense from that perspective.

My thoughts were coming from the following scheme:

  x::foo(…) // == foo.bind(x)(…)
            // == foo.[[call]](x, …)
new::foo(…) // == new foo(…)
            // == foo.[[construct]](…)

but I've come to the conclusion that neither is really logical.

Maybe making your generic newer function available as Function.prototype.new or Reflect.new would make more sense. The first would even allow using ::Foo.new.
Or just solve the underlying problem by adding a new method

Function.prototype.fromJSON = function(obj) {
    return Object.assign(new this, obj);
};

that should be overwritten by classes (via static fromJSON(obj) { … }) if necessary and that could be passed around via ::Foo.fromJSON.

bergus commented 8 years ago

It also is confusing since typeof and void which are in the same category of named unary operators are not talked about.

I don't think new is in the same category as these. Neither of them calls a function, and also they don't take arguments lists. In that regard, new is more like a binary function, that is going to get partially applied here. typeof, void, + and other operators that are truly unary would need function composition, not binding.

zenparsing commented 8 years ago

I think you got your statements backwards

I did, thanks : )

Maybe making your generic newer function available as Function.prototype.new or Reflect.new would make more sense. The first would even allow using ::Foo.new.

Nah, neither of those is really much of an improvement over the current situation as far as passing constructor factories around, since you would have to auto-bind Function.prototype.new or partially apply Reflect.new.

Foo::new, on the other hand, is just right ; )

bmeck commented 8 years ago

I don't think new is in the same category as these. Neither of them calls a function, and also they don't take arguments lists. In that regard, new is more like a binary function, that is going to get partially applied here. typeof, void, + and other operators that are truly unary would need function composition, not binding.

  • new is unary, it accepts a single operand, a function signature. unsure what you mean that it is binary? do you mean the fact that it evaluates the formals list?
  • as for function calls + could call .valueOf or even .toString.

Foo::new, on the other hand, is just right ; )

Disagree, ::new is a one off (wart). The more I think the more it looks like a square forced into a round hole.

::new even in a way would closer to global.new than one off for the new operator since it does identifier lookup in all other cases. Meta properties cannot have a valid identifier, and identifiers historically are always LHS arguments (within their expression) in JS.

Too many chances for confusion, please keep your proposal clean. I have listed more than 5 ways it can be confusing. Alternatives already exist for handling new, presenting a new way to do things just means more complexity to the language.

Falling back upon meta-properties as a reason that this makes sense seems a bit counter-intuitive. Meta properties are exposing VM state in a way consistent with existing state inspection. Function binding is partially applying (only the this value) of functions, which is already doable today, but lacks sugar. This is new syntax and would be the first time we have a RHS meta identifier.

I want a nicer function binding syntax, not one that introduces yet another exception to remember.

Just wait for MDN: The :: operator does ... , except in the case if new is on the RHS. Exceptions are bad; people don't like them in code, why do they want them in spec?

I have a poll in twitter (that I am not participating in), but am trying to gather votes one way or another on if the exception seems reasonable to other programmers.

dead-claudia commented 8 years ago

@bmeck It technically is, but in a sense, it's binding C.[[Construct]], the internal constructor method of C in this particular case.

bmeck commented 8 years ago

@isiahmeadows if this was true, the left hand only var f = ::new seems like it would work/use a global [[Construct]]. I simply don't view it that way.

dead-claudia commented 8 years ago

@bmeck I was referencing the [[Construct]] internal method used throughout the spec. And to be honest, it's always awkward using constructors in functional style, because of the new.

And BTW, Java has the same exact syntax for contructor references:

class Averager implements IntConsumer {
    private int total = 0;
    private int count = 0;

    public double average() {
        return count > 0 ? ((double) total) / count : 0;
    }

    public void accept(int i) {
        total += i;
        count++;
    }

    public void combine(Averager other) {
        total += other.total;
        count += other.count;
    }
}

Averager averageCollect = roster.stream()
    .filter(p -> p.getGender() == Person.Sex.MALE)
    .map(Person::getAge)
    .collect(Averager::new, Averager::accept, Averager::combine);

System.out.println("Average age of male members: " +
    averageCollect.average());

That was kinda my inspiration. But in all honesty, even though I came up with the syntax, I noted in the PR that I wasn't really holding on tight to it. I was more concerned about functionality than ergonomics.

bmeck commented 8 years ago

@isiahmeadows my argument is that:

Resulting in a different behavior is confusing. Thats a lot of edge casing, just to avoid making a generic way to call constructor functions.

Sure it looks nice, but I think it is out of scope for the initial proposal. If you want to specify a hidden new identifier that has [[Call]] I would be more open to it. This just smells without more clear explanation of what this is doing.

Specifying new as a function would make things much clearer and remove my concerns, but that poses its own problems. Until that point, those 4 points make this sound like people want pretty, not consistent.

zenparsing commented 8 years ago

I've moved the bound constructor stuff into a "Future Extensions" section. I've left the lookahead restriction in the grammar to allow support if we ever want it.

meandmycode commented 8 years ago

I think since there is seemingly a lot of debate about ::new then it's sensible to avoid it from the bind specification.

A somewhat related question to this debate, I have been making a lot of use of this new operator (back since abstract references) and plan to release libraries next year that imply using the bind operator, whilst things like babel are making it easy to use this now, I was looking around at what other 'near js' ecosystems are planning, such as typescript, one thing I noticed was that under the stage0 proposals for ecma262, the bind operator isn't flagged as being ready: https://github.com/tc39/ecma262/blob/master/stage0.md

Looking here there don't seem to be any blocking issues for the core binding operator syntax and was wondering if there are plans for how the operator will progress to the next stage, and if there is anything myself or anyone else can do to help here?

Thanks in advance,

dead-claudia commented 8 years ago

The main reason for ::new was a conceptual "binding the constructor" to make it more FP-friendly. For what it's worth, when I created the patch, it was only to get the concept and semantics formalized, independent of syntax (i.e. we're not even sure it's necessary, so it's kind of in a "phase 2 proposal" of sorts).

On Thu, Dec 24, 2015, 07:56 meandmycode notifications@github.com wrote:

I think since there is seemingly a lot of debate about ::new then it's sensible to avoid it from the bind specification.

A somewhat related question to this debate, I have been making a lot of use of this new operator (back since abstract references) and plan to release libraries next year that imply using the bind operator, whilst things like babel are making it easy to use this now, I was looking around at what other 'near js' ecosystems are planning, such as typescript, one thing I noticed was that under the stage0 proposals for ecma262, the bind operator isn't flagged as being ready: https://github.com/tc39/ecma262/blob/master/stage0.md

Looking here there don't seem to be any blocking issues for the core binding operator syntax and was wondering if there are plans for how the operator will progress to the next stage, and if there is anything myself or anyone else can do to help here?

Thanks in advance,

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

zenparsing commented 8 years ago

Closing this out, since we'd moved it to the "future extensions" section.