Inventing GObject ES6 classes

Hello again! If you’re a GJS user, I’d like your opinion and ideas. After my last post where I talked about new features coming in GNOME 3.26 to GJS, GNOME’s Javascript engine, I’m happy to say that the patches are nearly ready to be landed. We just need to figure out how to build SpiderMonkey 52 consistently even though Mozilla hasn’t made an official standalone release of it yet.

A top view of a latte next to a notebook with a pen, with coffee beans strewed artfully around.

A better literal depiction of JAVA SCRIPT I could not ask for… (Public domain image courtesy of Engin_Akyurt)

As I reported last time:

After that is done, I will refactor GJS’s class system (Lang.Class and GObject.Class). I believe this needs to be done before GNOME 3.26. That’s because [we will] gain ES6 classes, and I want to avoid the situation where we have two competing, and possibly incompatible, ways to write classes.

That’s what I’m busy doing now, in the run-up to GUADEC later this month, and I wanted to think out loud in this blog post, and give GJS users a chance to comment.

First of all, the legacy Lang.Class classes will continue to work. You will be able to write ES6 classes that inherit from legacy classes, so you can start using ES6 classes without refactoring all of your code at once.

That was the good news, now the bad

However, there is not an obvious way to carry over the ability to create GObject classes and interfaces from legacy classes to ES6 classes. The main problem is that Lang.Class and its subclasses formed a metaclass framework. This was used to carry out certain activities at the time the class object itself was constructed, such as registering with the GType system.

ES6 classes don’t have a syntax for that, so we’ll have to get a bit creative. My goals are to invent something (1) that’s concise and pleasant to use, and (2) that doesn’t get in the way when classes gain more features in future ES releases; that is, not too magical. (Lang.Class is pretty magical, but then again, there wasn’t really an alternative at the time.)

Here is how the legacy classes worked, with illustrations of all the possible bells and whistles:

The metaclass magic in Lang.Class notices that the class extends GObject.Object, and redirects the construction of the class object to GObject.Class. There, the other magic properties such as Properties and Signals are processed and removed from the prototype, and it calls a C function to register the type with the GObject type system.

Without metaclasses, it’s not possible to automatically carry out magic like that at the time a class object is constructed. However, that is exactly the time when we need to register the type with GObject. So, you pretty much need to remember to call a function after the class declaration to do the registering.

The most straightforwardly translated (fictional) implementation might look something like this:

The fictional GObject.registerClass() function would take the role of the metaclass’s constructor.

This is a step backwards in a few ways compared to the legacy classes, and very unsatisfying. ES6 classes don’t yet have syntax for fields, only properties with getters, and the resulting static get syntax is quite unwieldy. Having to call the fictional registerClass() function separately from the class is unpleasant, because you can easily forget it.

On the other hand, if we had decorators in the language we’d be able to make something much more satisfying. If you’re familiar with Python’s decorators, these are much the same thing: the decorator is a function which takes the object that it decorates as input, performs some action on the object, and returns it. There is a proposed decorator syntax for Javascript that allows you to decorate classes and class properties. This would be an example, with some more fictional API:

This is a lot more concise and natural, and the property decorators are similar to the equivalent in PyGObject, but unfortunately it doesn’t exist. Decorators are still only a proposal, and none of the major browser engines implement them yet. Nonetheless, we can take the above syntax as an inspiration, use a class expression, and move the registerClass() function around it and the GObject stuff outside of it:

Here, the body of the class is almost identical to what it would be with the decorator syntax. All the extra stuff for GObject is contained at the top of the class like it would be with the decorators. We don’t have the elegance of the property decorator, but this is quite an improvement on the first iteration. It’s not overly magical, it even acts like a decorator: it takes a class expression, and gives back a GObject-ized class. And when decorators eventually make it into standard Javascript, the basic idea is the same, so converting your code will be easy enough. (Or those who use transpiling tools can already go ahead and implement the decorator-based API.)

This is the best API I’ve been able to come up with so far. What do you think? Would you want to use it? Reply to this post or come talk to me in #javascript on GNOME IRC.

Next steps

Note first of all that none of this code exists yet. Depending on what feedback I get here, I hope to have a draft version working before GUADEC, and around the same time I’ll post a more detailed proposal to the javascript-list mailing list.

In addition, I will be speaking about this and more at GUADEC in my talk, “Modern Javascript in GNOME“. If you are attending, come talk to me there! Thanks to the GNOME Foundation for sponsoring my travel and accommodations.

Official badge from the GUADEC website: "I'm going to The GNOME Conference GUADEC Manchester, United Kingdom"

Advertisements

6 thoughts on “Inventing GObject ES6 classes

  1. > Having to call the fictional registerClass() function separately from the class is unpleasant, because you can easily forget it.

    Perhaps I’m misunderstanding the order of operations but is it not possible to have the GObject superclass automatically do the business of registerClass() so that all you need to remember is to call super(props) in the constructor? Or is the point that the properties and signals would not yet be initialized at that stage?

    Also, regarding the final proposal you have, what would happen if you started declaring properties on the ES6 class itself instead of (or in addition to) declaring them – correctly – in the Properties field. Would that generate an error/warning by GObject? Because I could see people, especially those used to ES6, doing that by mistake.

    • > Perhaps I’m misunderstanding the order of operations but is it not possible to have the GObject superclass automatically do the business of registerClass() so that all you need to remember is to call super(props) in the constructor? Or is the point that the properties and signals would not yet be initialized at that stage?

      Good question. I think it would be mostly feasible to do it that way, but not in every case. It would mean that a class only registers its type with GObject once an instance of that class is instantiated. I can see a few cases where that would break: (1) using class A as the type of a GObject.ParamSpec.object in another class B, means that constructing a B could fail depending on whether an A has already been constructed; and (2) GObject interfaces have to register their types but are never instantiated, so there would have to be some alternative mechanism for that.

      I think it could be done with some workarounds, but see e.g. https://bugzilla.gnome.org/show_bug.cgi?id=779843 for an example of what might go wrong.

      > what would happen if you started declaring properties on the ES6 class itself instead of (or in addition to) declaring them – correctly – in the Properties field.

      You get a normal Javascript property: not typed, and no notify signal. I think there are cases where you might actually want this. It was also possible with legacy Lang.Class.

  2. Why not just oldschool style like

    class MyClass extends GObject.Object {

    }
    MyClass.GTypeName = ‘MyNamespaceMyClass’;
    MyClass.Implements = [Gio.Initable, MyCustomInterface];
    MyClass.Properties = {
    ‘prop’: GObject.ParamSpec.int( /* etc., etc. */ ),
    };
    MyClass.Signals = {
    ‘signal’: { /* etc. */ },
    };

    ?
    And those who are brave to use transpiler will write

    class MyClass extends GObject.Object {
    static GTypeName = ‘MyNamespaceMyClass’;
    static Implements = [Gio.Initable, MyCustomInterface];
    static Properties = {
    ‘prop’: GObject.ParamSpec.int( /* etc., etc. */ ),
    };
    static Signals = {
    ‘signal’: { /* etc. */ },
    };
    }

    This syntax will be implemented by major JS engines pretty soon (decorators proposal depends on class fields). I think, it’s important to be future-compatible now and don’t invent syntax which will be thrown away soon.

    • I agree, being future-compatible is a really good point. However, I don’t want to do that at the expense of unsatisfying API in base GJS — there has to be a compelling reason for apps and extensions authors to switch to the new syntax, and most don’t have a transpiler integrated in their project yet.

      I’ve been thinking some more about how to make something that will work in the future with the desired decorator API with as few changes as possible, and I think it should be possible to make the function call work as both a decorator and a regular function call, as well as with class fields and without them, for authors who do wish to use transpilers.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s