cwal

s2: The new Keyword
Login

s2: The new Keyword

(⬑Table of Contents)

It's a Brand new Car()!

Jump to...

How to Use Your new Toy...

(The new keyword was added to the trunk on 20160121.)

Most object-oriented languages provide a language-level mechanism for creating new instances of client-defined types. s2, on the other hand, has historically (with the exception of object/array/hash literals) required that clients provide a factory function for their own types. Given s2's type system, both approaches are viable, but the new keyword was (eventually) added to help simplify script code usage by providing clients with a common approach to creating new instances, rather than each defining their own factory interfaces. This keyword superficially works similarly to JavaScript's new operator, but it also mixes in a bit of PHP-ness and has a quirk or two of its own which are side-effects of s2's prototypal class system.

Abstractly speaking, new's syntax is:

new Constructible(arguments…)

where the requirements and conventions for the "Constructible" part are described below. The arguments are processed just like any other function call arguments and get passed to the Constructible.

In script code, using new looks more or less like:

const MyType = {
  __new : function(...){...}
  // … other methods/properties …
};

var my = new MyType(1,2,3);
assert my inherits MyType;

Where MyType is a container type which implements an interface used by the new keyword: it must have a function property named __new.

A function conforming to that criteria is called a "constructor" function throughout this documentation. Such a type is said to be a "Constructible" type. If new is called on a value with no valid constructor (i.e. it is-not-a Constructible), an exception is thrown. typeinfo(cannew EXPR) can be used to figure out if calling new on a given operand is legal and typeinfo(isnewing) can tell the constructor function whether it's really being invoked via new, as opposed to being called "as a normal function".

Constructors are not inherited. That is, new T() only works if T itself has a constructor. If an object X sets T as its prototype, new X() will not work unless it defines its own constructor. There is currently no mechanism for passing constructor calls up to parent constructors. (When it's needed, it will be added.)

When the Constructible is called in the context of new, the following happens:

In the constructor call, this will be the newly-created Object and is the default result of the new call unless the constructor explicitly returns a non-undefined value. However, it is often desirable or necessary to use a different base type for new instances (in particular when deriving from C-native types). s2 cannot know, at the time the constructor is called, which "this" type you want, but it provides an approach which allows clients to tell it that:

In the latter case case:

Here are examples which demonstrate the different approaches…

The most basic case is that the client wants to have Object-typed results:

const MyClass = {
  __new: function(x,y){
    // this === a new Object.
    // *this.prototype* is the containing Object (===MyClass)
    this.x = x;
    this.y = y;
    // no explicit return is necessary:
    // Without a return, it behaves as if (return this) had been called.
  }
};

assert typeinfo(cannew MyClass);
assert new MyClass() inherits MyClass;

Be careful not to reference the MyClass symbol in such a constructor if that symbol is currently being declared (i.e. in the context of the var/const RHS): it won't exist until after the outer (being-defined) object finishes evaluating. Also remember that s2's symbol resolution rules mean that the symbolic name "MyClass" might not still be in scope when the constructor is run. i.e. don't ever rely on the prototype's symbolic name (the "class name"), if any.

If we want (or need) our new type to behave like Arrays, we can do the following:

const MyArray = {
  prototype: [].prototype, // inherit all array methods
  __new: function(){
    // *this* === a new Object, but we don't want that, so we need to…
    // (1) create our result value:
    const rc = [];
    // (2) set its prototype to our new "class":
    rc.prototype = this.prototype; // === MyArray
    // (3) perform any constructor-related work:
    // Here we simply clone the arguments:
    foreach(@argv=>v) rc[]=v;
    // (4) return our new value to replace the Object s2 created for us:
    return rc; // "new" will use rc as its result
  }
 // … add any new array-like methods …
};

assert typeinfo(cannew MyClass);
var a = new MyArray(1,2,3);
assert a inherits MyArray;
assert typeinfo(isarray a);
assert 3 === a.2;

Note that even though MyArray inherits from the Array prototype, that's not really what makes it an array: we still had to create a new array instance in the constructor, set its prototype to our "class," and return it so that new MyClass() would resolve to that array. Had we not reassigned rc.prototype, the returned value would not derive from MyArray. Had we not returned an Array instance, all instances of new MyArray() would not really be arrays at all, they would simply inherit the Array API (which wouldn't work for them because they wouldn't have a native-level array instance in their prototype chain). Returning an array, after reassigning its prototype to MyArray, means each MyArray instance is-an Array and can behave like one.

This swap-prototype-and-return approach is especially important when subclassing client-bound "native" types, so that the constructor can return a client-specified type.

More examples can be found in the new keyword's unit test script and some "stupid s2 tricks" using the new keyword can be found in this post.

Changing a class's typeinfo(name)

To change the name that typeinfo(name anInstance) reports for the prototype or instances of a class, simply assign the __typename property of the prototype to the desired name:

const MyClass = {
    __typename: __FLC,
    ...
};

Any given instance may override that property, if desired. If the value is not a string then it may be ignored in contexts which require the type's name to be a string.

Post-new Initialization

Similarly to Java, the new keyword allows a {code block} to follow the constructor call, and this block is run after the constructor completes, as if it were part of a member function taking no arguments and returning this:

new T(...) {
 // this === the new T instance (as returned by the ctor).
 // 'return' is not allowed: this is not a function call!
 // It's a scope, but any scope-level result value is discarded
 // (cleaned up with the scope).
}

Note that s2 uses a single pair of braces, though, not Java's {{…}} because adding those would require adding new token types (i.e. it would be more work just for the sake of looking like Java).

The above is functionally equivalent to, but more efficient than:

new T(...).withThis(proc(){
 // this === the new T instance.
 // 'return' *is* allowed: we're in a callback function now.
 // Unless we return a different (non-undefined) value,
 // the result of this callback is the same as the T constructor.
});

With two microscopic caveats: withThis() could potentially return a different value than the constructor did or (hypothetically) recurse into its own callback, whereas the post-new init block can do neither of those. A conceivable valid use case for such recursion is difficult to envision, but it can be done.

Note that new stops evaluating at the end of the parameter list or post-constructor initializer block, so it is legal to call methods on the returned object directly from new's result: e.g. new T().doFoo().

Because constructors can return any type (except undefined, which is indistinguishable from not explicitly returning and is used to signal the default behaviour), the following is also legal, but is neither efficient nor terribly useful:

const MyBoolean = {
  __new: function(v=throw "MyBoolean ctor requires an argument"/*[^31]*/){
    return !!v; // coerce v to a boolean value and return it
  }
};
assert true === new MyBoolean(0 < 1);
assert false === new MyBoolean(0 > 1);
assert catch {new MyBoolean()}.message.indexOf("MyBoolean ctor") === 0;

On the other hand, that could be used to implement singletons by returning a shared instance on each call:

const MyType = {
  __new: function(){
    instance.prototype || (instance.prototype = this.prototype);
    return instance;
  } using {instance: { prototype:undefined, … }}
};

Okay… so getting the prototype instance there is a bit tricky (because MyType is still being constructed, and is not yet in scope, when using resp. importSymbols() is called), but a slight reformulation works around that while also nicely demonstrating one of those esoteric uses for Object.withThis()

const MyType = {
  __new: function(){
    return instance;
  }
}.withThis(proc(){
  this.__new.importSymbols({
   instance: { prototype: this /* ha! */, …}
  });
});

Some of the high-level built-in s2 types implement the __new method3 and will behave intuitively:

var b = new s2.Buffer( … );
var h = new s2.Hash( … );
var p = new s2.PathFinder( … );

These classes also have factory methods called "new" (because s2 didn't always have the new keyword), but those do the exact same thing.

Objects, arrays, and strings do not have constructors because there's simply no need for them. Literals are more efficient and are (IMO) just as readable as, e.g. (new Object()). Feel free to make your own wrappers, as demonstrated in new's unit test script.

Implementing Anonymous Classes

Before we go, here's how to implement "anonymous classes" via new:

var x = new { __new: proc(){...} }( ctor args… );

i.e. the operand to new does not need to be an identifier, but can be an arbitrary expression (parens might be needed around it, depending on the construct). Which means that silliness like the following actually works…

s2sh> new new {__new:proc(){print("from C ctor:",argv); return \[^1]
  {__new:proc(){print("from internal ctor:",argv)}}}}(1,2,3)(4,5,5)
from C ctor: [1, 2, 3]
from internal ctor: [4, 5, 5]

s2sh> new while(1){ break new {__new:proc(){print("from Cctor:",argv); \
  return {__new:proc(){print("from internal ctor:",argv)}}}}(1,2,3)}(4,5,6)
from C ctor: [1, 2, 3]
from internal ctor: [4, 5, 6]

Don't try that at home, though. It's rough on the sanity, long-term.

Footnotes

(Reminder to self: footnote #30 was obsoleted by refactoring while porting this document from GDocs.)


  1. ^ What happens when we run (new MyType()) 1000 times in a loop and don't propagate the result out of the loop? The temporary/discarded "this" object gets recycled for every loop iteration after the first. Even the first iteration might have come from the recycling subsystem. i.e. it's very cheap in the aggregate.
  2. ^ s2 currently does not internally differentiate between not returning vs. returning undefined because for script purposes there has never been a reason to differentiate them (doing so would require new script-side semantics).
  3. ^ As a hidden property, incidentally, but that's not really significant.
  4. [^ 1 ]
    Pedantic note: the backslash there was added for formatting this
    page: s2sh does not support continuing lines that way.
  5. [^ 31 ]
    Yes, that's perfectly legal. A Happy Accident of the design.