I have an idea for a new feature for StratifiedJS. First, I will explain the problem that this feature is trying to solve.
In traditional OOP languages, you have "classes" which can have "attributes" (data) and "methods" (behavior). JavaScript does not have a traditional concept of classes, but its prototypical model is essentially the same: objects have attributes and methods, just like in a classical language.
However, putting methods into classes is brittle. I will now demonstrate this claim. Consider these classes:
function FileInput() {}
FileInput.prototype.read = function () { ... }
function Iterator() {}
Iterator.prototype.next = function () { ... }
function Book() {}
Book.prototype.read = function () { ... }
If we have some object "foo" and we want to treat it as an Iterator, we can just use "foo.next()". If "foo" is an Iterator it succeeds, but if "foo" isn't an Iterator, the method won't exist, so it will throw an error, which is exactly what we want. This is duck typing, hurray!
But what happens when we call "foo.read()"? Well, the answer is that it depends on whether "foo" is a FileInput or a Book. Since reading a Book is very different from reading a FileInput, we really want to distinguish these two cases. We can do so by using instanceof:
assert(foo instanceof FileInput)
foo.read()
By doing this we have guaranteed that "foo" is a FileInput, and thus the correct method will be called. And we can do the same whenever we want to treat "foo" as a Book:
assert(foo instanceof Book)
foo.read()
At first, this seems to work... until we define a new class:
function MyBook() {}
We want MyBook to behave like a Book, an Iterator, and a FileInput, all at the same time. Since we're using instanceof to distinguish between FileInput and Book, we can only make it work if MyBook is a subclass of FileInput:
MyBook.prototype = Object.create(FileInput.prototype)
But now it *isn't* a subclass of Book, so it will *only* work as a FileInput, and never as a Book. Some languages (like Python) introduce multiple inheritance to solve this problem. I think this is an insane hack: we already have classes, instances, attributes, methods, single-dispatch, single-inheritance, and now you want to add in multiple-inheritance too?! The language keeps getting more and more complicated.
What about duck typing? Well, if we just call "foo.read()" without checking the type, it *may* throw a nice error, or it may fail silently, or it may go and munge data... who knows. Duck typing isn't safe. We really would like our methods to fail fast if you call them with the wrong inputs.
And neither of those strategies solve the problem. Methods are named using strings, meaning `foo.read` is the same as `foo["read"]`. This means that *even* with multiple inheritance and/or duck typing, `Book.prototype.read` and `FileInput.prototype.read` will conflict with each other, because they use the same name.
It is hopeless. OOP failed. Multiple inheritance failed. Duck typing failed. But there is a solution! It just requires you to no longer treat methods as being defined inside classes (or in the case of JavaScript, defined on prototypes).
Well, if a method isn't defined on a class/prototype, where is it, then? And here is where my feature request comes in. I propose a new keyword called "generic". It looks like this:
generic foo;
Notice that we only gave it a variable name, nothing more. What this does is, it creates a new *generic function* and assigns it to the variable "foo". A generic function is *exactly* like a normal function... except the behavior of a generic function changes depending on the type of its first argument.
To define new behavior, we use the keyword "extend":
extend foo(x is Foo) {
return 1;
}
What the above means is, whenever you call "foo" with an argument which is instanceof "Foo"...
foo(new Foo())
...it will return 1. As a convenience, you can write this...
generic foo(x is Foo) {
return 1;
}
...which is exactly the same as this:
generic foo;
extend foo(x is Foo) {
return 1;
}
Now, let's add another rule:
extend foo(x is Bar) {
return 2;
}
Notice that we used "extend" rather than "generic". Using "generic" creates a *new* generic function. Using "extend" changes an *existing* generic function.
Now if we call "foo" with an argument that is instanceof "Bar", it will return 2. In other words...
foo(new Foo()) => 1
foo(new Bar()) => 2
If you think about it, this is exactly like methods! To understand what I mean, take the method "foo.read()" and replace it with the generic function "read(foo)". In both cases, depending on the type of "foo", we end up with different behavior.
The difference between generic functions and methods is... methods use strings to name things, so you end up with name collisions. But the generic function is put into an actual variable, so we can use the language's module system to prevent name conflicts!
Let's now solve the FileInput/Iterator/Book/MyBook problem using generic functions:
// file.sjs
function FileInput() {}
generic read(x is FileInput) { ... }
exports.FileInput = FileInput
exports.read = read
// iter.sjs
function Iterator() {}
generic next(x is Iterator) { ... }
exports.Iterator = Iterator
exports.next = next
// book.sjs
function Book() {}
generic read(x is Book) { ... }
exports.Book = Book
exports.read = read
Notice I'm now defining each type in a separate file. This wasn't necessary with methods, but is now necessary because both file.sjs and book.sjs define a variable called "read". Now, let us create MyBook:
// mybook.sjs
var file = require("./file")
var book = require("./book")
function MyBook() {}
extend file.read(x is MyBook) { ... }
extend book.read(x is MyBook) { ... }
exports.MyBook = MyBook
As you can see, we use StratifiedJS's built-in module system to allow us to extend both file.read and book.read. Now, when you treat a MyBook object as a book, it will behave in one way, and when you treat it as a file, it will behave in another way. This solves the problem completely! And unlike duck-typing, this is *safe*: if you try to call a generic function on something that it doesn't understand, it throws an error.
Now, let's suppose you release MyBook as a library, and people start to use it. People want to use MyBook as an iterator, but you (the author of mybook.sjs) didn't extend the iter.next function. That's okay, other people can extend it for you!
var iter = require("./iter")
var mybook = require("./mybook")
extend iter.next(x is mybook.MyBook) { ... }
With generic functions, it's trivial to define new behavior on old types, and new types that work with old behavior. Oh yeah, and it works really nicely with StratifiedJS's .. syntax:
@ = require(["./mybook", "./file", "./iter"])
new @MyBook() .. @read() .. @next()
Woah, it's almost like we're doing method calls, except it's safe and there's no name conflicts!
Here's the best part. You can get all this with ridiculous speed to boot. The ClojureScript team discovered a neat trick when they were trying to implement Clojure Protocols in JavaScript. I shamelessly stole their idea and applied it to generic functions. Here's how it works. This...
generic foo;
extend foo(x is Foo) { ... }
...will get compiled to this JavaScript:
// The "generic" and "extend" functions only need to be defined once.
// They can then be used to create/extend multiple generic functions.
var generic = (function () {
var id = 0;
return function (name) {
var key = "__unique_#{name}_#{++id}__";
function f(x) {
var method = x[key];
assert(typeof method === "function", "#{name} called on invalid type");
return method.apply(null, arguments);
}
f.__generic_key__ = key;
return f;
};
})();
var extend = function (gen, Type, f) {
var key = gen.__generic_key__;
assert(typeof key === "string", "extend can only be used on generic functions");
Type.prototype[key] = f;
};
var foo = generic("foo");
extend(foo, Foo, function (x) { ... });
What the?! Okay, to explain, let's look at some simplified code that does the same thing:
var foo = function (x) {
return x.__foo__.apply(null, arguments);
}
Foo.prototype.__foo__ = function (x) { ... }
So, what's going on is that a generic function is just a normal function that calls a method on its first argument (in this case, the method is "__foo__"). And then, extend will attach a __foo__ method to Foo.prototype. This is crazy, but it works and is really smoking fast.
Now, how is this different from the big blob of code up there? Well, first off, we can't use the method name "__foo__" because there might be other generic functions named "foo". So that's why we give it a unique method name, "__unique_foo_1__".
But now that we have given it a unique name, how does extend know which method name to use? Answer: the unique name is stored in foo.__generic_key__.
And then the "generic" and "extend" functions do a bit of checking so they throw errors if you try to call "foo" on something that it doesn't understand, or if you try to call "extend" on something that isn't a generic function.
Raw methods ended up being 3.63 times faster than generic functions with assertions. In my opinion, this is very acceptable.
Note: the benchmark was run with normal synchronous JavaScript. I should test its speed in StratifiedJS as well, because it may perform very differently due to SJS having continuations.
P.S. As you can see in the compiled JavaScript, it's actually possible to implement this right now as a library without any new syntax, it's just a bit clunky.
The syntax I proposed in here is decent, but I'm totally open to different syntaxes! Having good idiomatic support for generic functions is what's important, the exact syntax doesn't matter much.