On Mon, Mar 30, 2015 at 5:36 AM, Caitlin Potter <caitpotte...@gmail.com> wrote:
> >On Mar 30, 2015, at 1:49 AM, Allen Wirfs-Brock <al...@wirfs-brock.com> > wrote: > > >There is no intrinsic reason why we needed to mandate that class > constructors should throw when called. We even provided a simple and > straight forward way > >(new.target===undefined) that a ES constructor body can use to determine > whether it was called or new’ed. > > I don’t think it’s great to have branches in a constructor dealing with > this — it’s not super-obvious reading the code what it means (so it’s > another thing to train people to understand). > That's exactly my position. The co-mingling of [[Construct]] and [[Call]] in a single function was a side-effect of having all-singing, all-dancing functions. The nice thing about ES6 is that we got dedicated syntax for classes and callbacks. Just because we *can* make constructor functions serve double-duty via a reflective mechanism doesn't mean that's the right thing to do. > A better way (which I think has been suggested by someone already in a > different thread), would be to have a separate “magic” method to provide > `call` code. > > ```js > class Buffer { > constructor(…a) { > // … > } > > factory(…a) { // [@@factory](), __factory__(), whatever > return new Buffer(…a); > // Or whatever else one might wish to do in a factory method > } > } > ``` > That was the proposal I made that Allen alluded to: ```js class Buffer { constructor(…a) { // … } [Symbol.call](a) { if (typeof a === 'string') { return Buffer.fromString(a); } return new Buffer(…arguments); } } ``` > > But, I think the factory problem is solved well enough with static methods > > ```js > class Buffer { > constructor(…a) { > this.initialize(…a); > } > > // Much easier to understand these, compared with Buffer(someBuffer) or > Buffer(someArray) etc > static withBuffer(buffer) { assert(Buffer.isBuffer(buffer)); return new > Buffer(buffer); } > static withArray(array) { assert(Array.isArray(array)); return new > Buffer(array); } > static withSize(size) { assert(IsUInt(size)); return new Buffer(size); } > static fromString(str, encoding = “utf8") { assert(IsString(str) && > IsString(encoding)); return new Buffer(str, encoding); } > > initialize(…a) { > switch (a.length) { > case 1: > if (IsUInt(a[0])) return allocateBufferOfSize(this, a[0]); > else if (Array.isArray(a[0]) return allocateBufferFromArray(this, > a[0]); > else if (Buffer.isBuffer(a[0]) return allocateCopyOfBuffer(this, > a[0]); > else if (IsString(a[0]) { /* fall through */ } > else ThrowTypeError(“Function called with incorrect arguments!"); > case 2: > if (IsUndefined(a[1]) a[1] = “utf8”; > if (IsString(a[0] && IsString(a[1])) return > allocateBufferFromString(this, a[0], a[1]); > default: > ThrowTypeError(“Function called with incorrect arguments!"); > } > } > } > ``` > I agree that static methods are sufficient, but I also agree that it would be nice to be able to describe existing built-in APIs in terms of classes. That doesn't, however, mean that we need to force both use-cases into a single function called *constructor*. I feel strongly that this: ```js class Buffer { constructor(from) { // switch on Number, isArray, or Buffer } [Symbol.call](from, encoding='utf8') { if (typeof from === 'string') { return Buffer.fromString(from, encoding); } return new Buffer(from); } } ``` is clearer than: ```js class Buffer { constructor(from, encoding='utf8') { if (!new.target) { if (typeof from === 'string') { return Buffer.fromString(from, encoding); } } // switch on Number, isArray, or Buffer } } ``` For one thing, it requires the reader to know that `new.target` is being used to determine whether the constructor was called with `new`. While it certainly is expressive enough, it's an unusual reflective operation that doesn't exactly say what you mean. For another, putting two uses into a single method and separating them by an `if` is quite often a hint that you want to break things up into two methods. I think that's the case here. One of the nice things about the `[Symbol.call]` method is that a reader of the class can determine at a glance whether it handles [[Call]], and not have to scan the constructor to see if (and how!) `new.target` is used. And since `new.target` can also be used for other usages, a reader unfamiliar with the pattern might not even have a good query to Google (searching "what is new.target for in JavaScript", even if that works at all, might likely bring up a bunch of articles about implementing base classes). >I think we should just drop that throws when called feature of class > constructors.. > > > >(The restriction was added to future proof for the possibility of > inventing some other way to provide a class with distinct new/call > behavior. I don’t think we need nor can afford to > >wait for the invention of a new mechanism which will inevitably be more > complex than new.target, which we already have.) > I'll bring up `[Symbol.call]` at the next meeting. It would be quite helpful if you would enumerate the areas in which you expect it to be complex, so I can make sure to address them in my proposal. > I’m all for it if it can be allowed without making classes more > complicated for consumers to use — The thing I like about requiring `new` > is that it’s very simple and straight forward. > The reason we dropped it was precisely because several of us felt that the cryptic `if (new.target)` check was a throwback to the original all-in-one design of functions, and that the new class syntax gives us the breathing room we need to describe things in a clear way without losing expressiveness. > But in either case, these (IsCallable / IsConstructor) are pretty basic > qualities of objects that a Reflection* api ought to be able to read into, > imho. > What Allen is saying is that the implementation of "throw if constructor" doesn't work by not implementing [[Call]], but rather by implementing [[Call]] to throw, so those reflective APIs would say the wrong thing, and that this is observable via proxies. Allen, can you say more about why you spec'ed it that way? > > > > > > >> On Mar 29, 2015, at 11:51 PM, Caitlin Potter <caitpotte...@gmail.com> > wrote: > >> > >> ... > >> > >> Reflect.isConstructor(fn) -> true if Class constructor, generator, or > legacy (and non-builtin) function syntactic form > >> Reflect.isCallable(fn) -> true for pretty much any function, except for > class constructors and a few builtins > > > > I’ve already seen another situation (node’s Buffer) where code could be > simplified by using a ES6 class definition but where that is prevented > because a class constructor throws when called. > > > > Just to clarify something. Class constructors actually are “callable”. > You can observe this by the fact that Proxy allows you to install an > “apply” handler (the reification of the [[[Call]] internal method) on a > class constructor. The the fact that an object can be [[Call]]’ed is > already reflected by the typeof operator. Class constructors throw when > called because at the last minute we choose to make their [[Call]] do an > explicit throw not because they aren’t callable. > > > > There is no intrinsic reason why we needed to mandate that class > constructors should throw when called. We even provided a simple and > straight forward way (new.target===undefined) that a ES constructor body > can use to determine whether it was called or new’ed. > > > > I think we should just drop that throws when called feature of class > constructors.. > > > > (The restriction was added to future proof for the possibility of > inventing some other way to provide a class with distinct new/call > behavior. I don’t think we need nor can afford to wait for the invention of > a new mechanism which will inevitably be more complex than new.target, > which we already have.) > > > > Allen > > > > > > _______________________________________________ > es-discuss mailing list > es-discuss@mozilla.org > https://mail.mozilla.org/listinfo/es-discuss >
_______________________________________________ es-discuss mailing list es-discuss@mozilla.org https://mail.mozilla.org/listinfo/es-discuss