Type specifications in Parallel JS
3 September 2013
Since I last wrote, we’ve made great progress with the work on the Parallel JS and Typed Objects (nee Binary Data) implementation. In particular, as of this morning, preliminary support for typed objects has landed in Mozilla Nightly, although what’s currently checked in is not fully conformant with the current version of the standard (for this reason, support is limited to Nightly and not available in Aurora or Beta builds).
Meanwhile, we’ve been fixing small bugs in the existing Parallel JS
support code and also working on a prototype of the new API. There is
still some amount of work left to do with typed objects before we can
eliminate the old ParallelArray
code entirely: in particular, we
have to implement handles and make further progress on the JIT
integration.
We’ve also been working hard with the Rivertrail team at Intel on figuring out what the final API will look like. My prior post sketched out the basic design we’ve been working with. But the devil’s in the details, so the latest work has been trying to figure out precisely what the methods look like and so forth. One of the recent shifts that looks like it will be necessary is to change how the return types from PJs methods are specified. This is due to a non-obvious interaction with typed object prototypes that I want to describe in this post.
Type objects and prototypes
To begin, let me briefly explain how the typed objects API works with prototypes. Whenever you create a new type object – that is, a descriptor for a type – it has an associated prototype. This can be used to add methods to the instances of the type object in the usual way:
var ColorType1 = new StructType({r: uint8, g: uint8, b: uint8});
ColorType1.prototype.average = function() {
return (this.r + this.g + this.b) / 3;
}
var white1 = new ColorType1({r: 255, g: 255, b: 255});
var avg = white1.average(); // returns 255
If I go off and define an equivalent struct type somewhere else, it will nonetheless have a distinct prototype and therefore a distinct set of methods:
var ColorType2 = new StructType({r: uint8, g: uint8, b: uint8});
var white2 = new ColorType2({r: 255, g: 255, b: 255});
var avg = white2.average(); // ERROR
Implications for the PJs API
The fact that otherwise equivalent type objects have distinct prototypes has concrete implications for our PJs API design. We had originally been contemplating an API in which users provided the component types and we would synthesize them into a final type. For example, if you have an array of pixels and you want to map it into an array of doubles, you might have written code like:
var ColorType = new StructType({r: uint8, g: uint8, b: uint8});
var ImageType = new ArrayType(ColorType, 1024*768);
var myImage = new ImageType();
var averages = myImage.mapPar(c => (c.r + c.g + c.b) / 3, uint8);
The interesting line is the final one, which maps from an array of
pixels into an array of uint8
. The second argument to map here
specified the return type of the closure (this argument is optional;
if you omit it, we would use the type specification any
, meaning any
kind of value can be returned).
In this style of API, the type of the value returned from map would
then by (internally) created as new ArrayType(uint8, 1024*768)
. This
is fine if equivalent types all behave in exactly the same way. But
what if the user wanted to add methods to the prototype of the
returned value? Since mapPar
would presumably be creating a fresh type
object, each result would also have a fresh prototype, which would be
both expensive (many objects being allocated) and not useful (no way
to add methods).
So instead we should take an approach where we always specify the final return type of the function. That means that the code above would be changed slightly to read like so:
var ColorType = new StructType({r: uint8, g: uint8, b: uint8});
var ImageType = new ArrayType(ColorType, 1024*768);
var AverageType = new ArrayType(uint8, 1024*768);
var myImage = new ImageType();
var averages = myImage.mapPar(c => (c.r + c.g + c.b) / 3, AverageType);
You can see that we introduced a new type (AverageType
) which is an
array of the same length as ImageType
but with a different element
type (uint8
vs ColorType
). Now we can use this type as the type
annotation for the call to mapPar
. Furthermore, users can add their
own methods to AverageType.prototype
as they choose.