Prefix takes One Giant Leap with Support for Generics

Tuesday, September 6th, 2011     #javascript #everything

After a week or so of mind melting work, Prefix now support Generics! This was a challenge both in compiling the Prefix code and in working out how to generate the required JavaScript. But since there's not much point to a strongly typed language without strongly typed collections, I'm sure the effort is worth it...

This post is about Prefix a modern language that compiles to JavaScript. A language where C# is the inspiration - not the goal. Read more on the Prefix Project Page.

What Are Generics?

For this post I'm going to assume you're already familiar with C#'s generic support. If you're not, there are plenty of references and tutorials available online (here's one from Microsoft). In short generics are a way to declare strongly typed classes, interfaces and methods that defer the specification of one or more types until the class or method is used in code

Generics in Prefix

Prefix's supports most of the C# generics capabilities including:

  • multiple type parameter
  • constraints - base class, required interfaces, new() and class
  • generic classes, intefaces and methods
  • nested generic types
  • parameter type inference for generic functions

To support this functionality there's up to about 1.5kb of automatically embedded and minified runtime code. In many cases however generics can be used with no runtime library requirements - in which case the Prefix compiler will leave it out.

A Simple Generic Class

Here's an example of a simple generic list class: (see further down for notes)

{{C#}}
// no warnings
extern dynamic Console;

class MyList<T>
{
    public void Add(T t)
    {
        _data.push(t);
    }

    public T GetAt(int index)
    {
        return (T)(`this._data[index]`);
    }

    public int Count
    {
        get
        {
            return _data.length;
        }
    }

    T[] _data = (T[])(`[]`);
}

var l = new MyList<int>();
Console.WriteLine(l.Count);
l.Add(10);
l.Add(20);
l.Add(30);
Console.WriteLine(l.Count);

for (int i=0; i<l.Count; i++)
{
  Console.WriteLine(l.GetAt(i));
}
{{JavaScript}}
(function() {

// class MyList<T>
var MyList$1 = function()
{
    this._data = [];
};
MyList$1.typeName = 'MyList$1';
MyList$1.prototype._data = null;

MyList$1.prototype.Add = function(t)
{
    this._data.push(t);
};

MyList$1.prototype.GetAt = function(index)
{
    return this._data[index];
};

MyList$1.prototype.get_Count = function()
{
    return this._data.length;
};

// Start Global Code
var l=new MyList$1();
Console.WriteLine(l.get_Count());
l.Add(10);
l.Add(20);
l.Add(30);
Console.WriteLine(l.get_Count());
for (var i=0; i<l.get_Count(); i++)
{
    Console.WriteLine(l.GetAt(i));
}

})();

Note the following:

  • The JavaScript class name is qualified with the number of type arguments (ie: MyList$1). This is because its possible to have multiple generic classes, each with a different number of generic arguments. MyList<T,U> would be converted to MyList$2 etc...
  • Arrays are implemented as a new built-in system generic Array<T>. Unlike C# arrays but like JavaScript arrays, Prefix arrays can be resized after being created.
  • Since I don't have complete language level operability with JavaScript arrays yet, I've had to resort to using backtick escapes to get this example to work. (see GetAt and the _data initializer in the Prefix code.)
  • There's no runtime overhead with this example - all type checking is done at compile time.

An example with Runtime Dependencies

As in the above example, many generic types - especially collection classes - don't require any runtime type information about the generic type arguments and therefore translate fairly cleanly to JavaScript. When you do need runtime type information things get more complicated, but I've tried to hide away the messy details as much as possible.

Consider this example that can create instances of a type declared through a generic parameter:

{{C#}}
extern dynamic Console;

class Foo
{

}

class MyFactory<T> where T:new()
{
   public T CreateInstance()
   {
       return new T();
 }
}

var foo = new MyFactory<Foo>().CreateInstance();
Console.WriteLine(foo is Foo);
{{JavaScript}}
(function() {

// Prefix runtime library
var $dex = {};
$dex.makeCachedGeneric=/* removed */ 
$dex.resolveGenericArg=/* removed */
$dex.resolveGenericArgs=/* removed */
$dex.setupGenericClass=/* removed */

// class Foo
var Foo = function() {};
Foo.typeName = 'Foo';

// class MyFactory<T>
var MyFactory$1 = $dex.setupGenericClass(1, null, null, null, function() {});
MyFactory$1.typeName = 'MyFactory$1';

MyFactory$1.prototype.CreateInstance = function()
{
  var T = this.closedClass$1.genericArgs[0];
  
    return new T();
};

// Start Global Code
var foo=(new (MyFactory$1.makeGeneric([Foo]))()).CreateInstance();
Console.WriteLine((foo)!==null);

})();

Note:

  • I've removed the runtime library functions because they're messy and distract from the interesting stuff.
  • The MyFactory$1 class is initialized using the runtime function setupGeneric which does everything need to turn this into a generic class with runtime information about the generic args.
  • MyFactory<Foo> becomes MyFactory$1.makeGeneric([Foo]).
  • makeGeneric creates a new class (if not already created) that knows about Foo as the type argument
  • Inside CreateInstance you can see T is retrieved via a this.closedClass$1 which is setup by the call to makeGeneric.

As you can see, it's a little messy but quite powerful. I've tried to make the JavaScript code mirror as closely as possible the original Prefix code - mainly it's the makeGeneric call that messes things up - everything else is reasonably out of the way.

This additional runtime code is required when:

  • the generic class contains a new T()
  • using dynamic casts (as and is) to/from T
  • using default(T) and T is not known to be a reference type (ie: no T has no constraints)
  • A generic class has static fields (since each set of generic arguments gets its own copy of the variable)

More Examples

Rather than post detailed examples of everything here, feel free to checkout the latest unit test results for more detail. There are examples of generic functions, generic classes with static members, nested generics and more. If you're adventurous, the test cases also include the uncensored JavaScript with the runtime library support functions in all their gory detail.

In conclusion I've tried to make Prefix's generics support powerful but not at the expense of too much interference in the generated JavaScript code. Although I'd like the JavaScript to be cleaner I'm not sure what else could be done to improve it.

Generics is a huge step forward for Prefix and paves the way for a number of other features including arrays and enumerating collections with foreach statements.

« Older - Prefix - Generics (again), indexers, read only fields, variadic parameters Newer - Unit Testing Prefix with V8, V8Sharp and PetaTest (and hints on what's coming next) »