Proposal: Improve syntax for inline anonymous class instantiations

# Igor Vaynberg (7 years ago)

Given a simple class with an abstract method "populateItem"

class ArrayView extends Container {
    constructor(id, model) {
        super(id);
        this.model = model;
    }

    // methods referencing "populateItem" omitted for clarity
}

the current anonymous instantiation syntax looks like this:

this.add(new class extends ArrayView {
    populateItem(item) {
        item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
        item.add(new Label("title", new PropertyModel(item.model, "title")));
    }
}
("items", itemsModel)
);

The problem with this syntax is that it pushes the constructor parameters below the class body which I think causes two problems:

When scanning code constructors often contain the piece of information that helps locate the anonymous class, which currently requires the developer to look back. This is especially problematic for anonymous classes with long class bodies.

When writing code I usually think about the constructor first, so it seems it would be preferable to write it before moving onto working on the class body. This is also the reason why constructors are usually placed toward the top of named classes' source.

A better syntax would move the constructor parameters between the super class name and the class body:

this.add(new class extends ArrayView("items", itemsModel) {
    populateItem(item) {
        item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
        item.add(new Label("title", new PropertyModel(item.model, "title")));
    }
});

If possible it would also be great to get rid of the "class extends" keywords for this usecase:

this.add(new ArrayView("items", itemsModel) {
    populateItem(item) {
        item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
        item.add(new Label("title", new PropertyModel(item.model, "title")));
    }
});

Thoughts?

# Matthew Robb (7 years ago)

For some reason this idea made me think about adding extends to function syntax:

class Foo {}

function Bar(a, b) extends Foo {
  // ...
}

// Basic sugar for

function Baz(a, b) {
  // ...
}

Object.setPrototypeOf(Baz, Foo);
Object.setPrototypeOf(Baz.prototype, Foo.prototype);

Although now that I re-read the original post I don't think this addresses the same scenario

# Allen Wirfs-Brock (7 years ago)
(new class extends foo(bar) {…})

is already valid syntax that means use the value return from calling foo with argument bar as the superclass of the class that is being instantiated. What you propose would be a breaking change.

# Igor Vaynberg (7 years ago)

What about limiting it just to (new foo(bar) {...}) syntax?

# T.J. Crowder (7 years ago)

Two aspects to this: Motivations and syntax.

On motivations:

Addressing new syntax, the first question has to be: Is this use case sufficiently common and painful that it needs new syntax? The answer may be yes, but we need to ask the question.

Trying to solve it without new syntax with a helper function, I've come up with three ways of getting those arguments up front. I'm not all that happy with any of them.

Helper #1:

This kind of has the opposite problem (the class is hidden away at the end), but it's dead simple:

this.add(makeInstance("items", itemsModel, class extends ArrayView {
    populateItem(item) {
        item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
        item.add(new Label("title", new PropertyModel(item.model, "title")));
    }
}));

...where makeInstance is:

const makeInstance = (...args) => {
    return new args[args.length - 1](...args.slice(0, args.length - 1));
};

Helper #2:

Puts things in the desired order, but isn't exactly elegant at point-of-use:

this.add(makeInstance(ArrayView, "items", itemsModel, parent => class extends parent {
    populateItem(item) {
        item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
        item.add(new Label("title", new PropertyModel(item.model, "title")));
    }
}));

...where makeInstance is:

const makeInstance = (parent, ...rest) => {
    const args = rest.slice(0, rest.length - 1);
    const subclass = rest[rest.length - 1](parent);
    return new subclass(...args);
};

Helper #3:

Gets really close in terms of usage, but because the methods would have the wrong [[HomeObject]] (Igor's example didn't use super in populateItem but presumably it could have), it has to resort to not one but two setPrototypeOf calls, which is just ugly:

this.add(makeInstance(ArrayView, "items", itemsModel, {
    populateItem(item) {
        item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
        item.add(new Label("title", new PropertyModel(item.model, "title")));
    }
}));

...where makeInstance is:

const makeInstance = (cls, ...rest) => {
    const args = rest.slice(0, rest.length - 1);
    const methods = rest[rest.length - 1];
    const subclass = class extends cls { };
    Object.setPrototypeOf(methods, cls.prototype);
    Object.setPrototypeOf(subclass.prototype, methods);
    return new subclass(...args);
};

(Hey, I warned you it was ugly.) (This isn't the first time I've wanted to change a method's [[HomeObject]] after creation.)

Maybe someone else can do better.

Or, of course, just don't use an anonymous class.

On syntax:

Looking at:

this.add(new ArrayView("items", itemsModel) {
    populateItem(item) {
        item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
        item.add(new Label("title", new PropertyModel(item.model, "title")));
    }
});

A couple of notes/observations:

  • This is exactly how Java handles doing the same thing. JavaScript is not Java, of course, but worth noting prior art.

  • It would be good to hear early from implementers if this presents a parsing challenge. Until the {, it looks like you're instantiating ArrayView with "items" and itemsModel; the { then changes things. I don't know if that's backtracking, or looking ahead, or what. Obviously Java parsers handle it, but again, different language, different challenges.

  • If you remove the this.add(...) part of that, there's an ASI hazard if you put a line break before the { -- it becomes new ArrayView("items", itemsModel); followed by a block (which then has invalid content). (Not an insurmountable problem, it's not like it's the only ASI hazard caused by a line break before a {. But it would add another one.)

-- T.J.

# Alexander Jones (7 years ago)

With super() and closure binding of the anonymous class constructor (as with all class methods) you can basically solve your problem of constructor arguments appearing in the wrong place:

this.add(
    new class extends ArrayView {
        constructor() { super("items", itemsModel); }
        populateItem(item) {
            item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
            item.add(new Label("title", new PropertyModel(item.model, "title")));
        }
    }
);

I concede that spelling constructor, super, and the various soup of punctuation is a little less than ideal, but at the end of the day I think this is quite reasonable, don't you?

# Igor Vaynberg (7 years ago)

On Jan 7, 2017, at 4:45 AM, Alexander Jones <alex at weej.com> wrote:

Hi Igor

With super() and closure binding of the anonymous class constructor (as with all class methods) you can basically solve your problem of constructor arguments appearing in the wrong place:

this.add(
    new class extends ArrayView {
        constructor() { super("items", itemsModel); }
        populateItem(item) {
            item.add(new Checkbox("check", new PropertyModel(item.model, "done")));
            item.add(new Label("title", new PropertyModel(item.model, "title")));
        }
    }
);

I concede that spelling constructor, super, and the various soup of punctuation is a little less than ideal, but at the end of the day I think this is quite reasonable, don't you?

Yes, indeed. Thanks Alexander. I don't know why I didnt think of that myself. It is a little soupy since it adds an extra line and four extra keywords, but at least it allows this style of code.

Thanks again,