Determine if a value is Callable/Constructible

Yehuda Katz wycats at gmail.com
Mon Mar 30 14:59:42 UTC 2015


On Mon, Mar 30, 2015 at 5:36 AM, Caitlin Potter <caitpotter88 at gmail.com>
wrote:

> >On Mar 30, 2015, at 1:49 AM, Allen Wirfs-Brock <allen at 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 <caitpotter88 at 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 at mozilla.org
> https://mail.mozilla.org/listinfo/es-discuss
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.mozilla.org/pipermail/es-discuss/attachments/20150330/4567292e/attachment-0001.html>


More information about the es-discuss mailing list