Prototypes vs lexical scope in the Ceylon JavaScript compiler

In the previous post I introduced the Ceylon JavaScript compiler project. One of the things I mentioned was that there was an extremely natural mapping from Ceylon to JavaScript making use of JavaScript's lexical scope to create nicely encapsulated JavaScript objects. For example, given the following Ceylon code:

shared class Counter(Integer initialCount=0) {
    variable value currentCount:=initialCount;
    shared Integer count {
        return currentCount;
    }
    shared void inc() {
        currentCount:=currentCount+1; 
    }
}

We produce the following JavaScript:

var $$$cl15=require('ceylon/language/0.1/ceylon.language');

//class Counter at members.ceylon (1:0-9:0)
this.Counter=function Counter(initialCount){
    var $$counter=new CeylonObject;

    //value currentCount at members.ceylon (2:4-2:45)
    var $currentCount=initialCount;
    function getCurrentCount(){
        return $currentCount;
    }
    function setCurrentCount(currentCount){
        $currentCount=currentCount;
    }

    //value count at members.ceylon (3:4-5:4)
    function getCount(){
        return getCurrentCount();
    }
    $$counter.getCount=getCount;

    //function inc at members.ceylon (6:4-8:4)
    function inc(){
        setCurrentCount(getCurrentCount().plus($$$cl15.Integer(1)));
    }
    $$counter.inc=inc;

    return $$counter;
}

Notice that this code is really quite readable and really not very different to the original Ceylon.

Let's load this module up in the node REPL, and play with the Counter.

> Counter = require('./node_modules/default/members').Counter
[Function: Counter]
> Integer = require('./runtime/ceylon/language/0.1/ceylon.language').Integer
[Function: Integer]
> c = Counter(Integer(0))
{ getCount: [Function: getCount], inc: [Function: inc] }

The Counter instance presents a nice clean API with getCount() and inc() functions:

> c.getCount().value
0
> c.inc()
> c.inc()
> c.getCount().value
2

Notice that the actual value of $$counter is completely hidden from the client JavaScript code. Another nice thing about this mapping is that it is completely free of JavaScript's well-known broken this. I can freely use the methods of c by reference:

> inc = c.inc
[Function: inc]
> count = c.getCount
[Function: getCount]
> inc()
> count().value
3

Now, an issue that was bugging me about this mapping - and bugging Ivo even more - is the performance cost of this mapping compared to statically binding the methods of a class to its prototype. Ivo did some tests and found that it's up to like 100 times slower to instantiate an object that defines its methods in lexical scope instead of using its prototype on V8. Well, that's not really acceptable in production, so I've added a switch that generates code that makes use of prototypes. With this switch enabled, then for the same Ceylon code, the compiler generates the following:

var $$$cl15=require('ceylon/language/0.1/ceylon.language');

//ClassDefinition Counter at members.ceylon (1:0-12:0)
function $Counter(){}

//AttributeDeclaration currentCount at members.ceylon (2:4-2:45)
$Counter.prototype.getCurrentCount=function getCurrentCount(){
    return this.currentCount;
}
$Counter.prototype.setCurrentCount=function setCurrentCount(currentCount){
    this.currentCount=currentCount;
}

//AttributeGetterDefinition count at members.ceylon (3:4-5:4)
$Counter.prototype.getCount=function getCount(){
    return this.getCurrentCount();
}

//MethodDefinition inc at members.ceylon (6:4-8:4)
$Counter.prototype.inc=function inc(){
    this.setCurrentCount(this.getCurrentCount().plus($$$cl15.Integer(1)));
}

this.Counter=function Counter(initialCount){
    var $$counter=new $Counter;
    $$counter.initialCount=initialCount;        
    return $$counter;
}

Clearly this code is a bit harder to understand than what we started with. It's also a lot uglier in the REPL:

> c = Counter(Integer(0))
{ initialCount: { value: 0, ... } }

Notice that the internal state of the object is now exposed to clients. And all its operations - held on the prototype - are also accessible, even the non-shared operations. Finally, JavaScript's this bug is back:

> inc = c.inc
[Function: inc]
> inc()
TypeError: Object #<error> has no method 'getCurrentCount'
    at inc (/Users/gavin/ceylon-js/build/test/node_modules/default/members.js:21:31)
    ...

We have to use the following ugly workaround:

> inc = function(){c.inc.apply(c,arguments)}
> inc()
> c.getCount().value
1

(Of course, the compiler automatically inserts these wrapper functions when you write a function reference at the Ceylon level.)

Personally, I don't really see why the JavaScript interpreter in V8 could not in principle internally optimize our original code to something more like our "optimized" code. I think it would make JavaScript a much more pleasant language to deal with if there wasn't such a big difference in performance there.

Anyway, if you're producing your JavaScript by writing Ceylon, this is now just a simple compiler switch :-)