Supporting mixins and c# like attributes

Topics: General
Oct 7, 2012 at 7:44 PM
Edited Oct 9, 2012 at 2:02 AM

There's been a bit of talk on the forums around supporting mixins in TypeScript and with the 0.8 drop you have to use interfaces to achieve this.  I wanted to see if I could streamline the process of defining mixins a bit and what I ended up creating is a base class that add support for not only defining mixins but also c# like attributes.  Copy the code below into the playground and run it to see that it works:

update: see the simplified approach to mixins that I posted a few messages down.

// Base class & interfaces for supporting attributes & mixins
class ObjectBase {
    constructor () {
        // Invoke attributes
        var _constructor = (<any>this).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                var _attribute = _constructor.__attributes__[i];
                if (_attribute.onCreated) {
                    _attribute.onCreated(this);
                }
            }
        }
    }

    // Static methods

    static attribute(type: new () => ObjectBase, attribute: IAttribute): void {
        if (!(<any>type).__attributes__) {
            (<any>type).__attributes__ = [];
        }
        (<any>type).__attributes__.push(attribute);
    }

    static hasAttribute(obj: any, attribute: new () => IAttribute): bool {
        var _constructor = (<any>obj).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                if (_constructor.__attributes__[i] instanceof attribute) {
                    return true;
                }
            }
        }
        return false;
    }

    static mixin(type: new () => ObjectBase, mixin: IMixin): void {
        mixin.extend(type.prototype);
        if (mixin.onCreated) {
            attribute(type, mixin);
        }
    }
}

interface IAttribute {
    onCreated? (obj: any): void;
}

interface IMixin extends IAttribute {
    extend(prototype: any): void;
}

// Example attribute pre-binds callback methods to objects this pointer

class CallbacksAttribute implements IAttribute {
    constructor (private prefix: string) {
    }

    public onCreated(obj: any): void {
        var _constructor = obj.constructor; 
        if (!_constructor.__cb__) { 
            _constructor.__cb__ = {}; 
            for (var m in obj) { 
                var fn = obj[m]; 
                if (typeof fn === 'function' && m.indexOf(this.prefix) == 0) { 
                    _constructor.__cb__[m] = fn;                     
                } 
            } 
        } 
        for (var m in _constructor.__cb__) { 
            (function (m, fn) { 
                obj[m] = function () { 
                    return fn.apply(obj, Array.prototype.slice.call(arguments));                       
                }; 
            })(m, _constructor.__cb__[m]); 
        } 
    }

    static addTo(type: new () => ObjectBase, prefix = 'cb_'): void {
        if (!(prefix in cache)) {
            cache[prefix] = new CallbacksAttribute(prefix);
        }
        ObjectBase.attribute(type, cache[prefix]);
    }
	static cache: { [x: string]: CallbacksAttribute; } = {};
}

// Example mixin makes objects disposable

interface IDisposable {
	dispose (): void;
	isDisposed (): bool;
}

class DisposableMixin implements IMixin {
    public extend(prototype: IDisposable): void {
        prototype.dispose = function (): void {
            if (!this._isDisposed) {
                this._isDisposed = true;
                if (this.onDispose) {
                    this.onDispose();
                }
            }
        }

        prototype.isDisposed = function (): bool {
            return this._isDisposed;
        }
    }

    public onCreated(obj: any): void {
        obj._isDisposed = false;
    }

    static addTo(type: new () => ObjectBase): void {
        if (!mixin) {
            mixin = new DisposableMixin();
        }
        ObjectBase.mixin(type, mixin);
    }
	static mixin: DisposableMixin = null; 
}


// Example attribute usage

class Foo extends ObjectBase {
	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}
CallbacksAttribute.addTo(Foo);

var foo = new Foo();
foo.cb_test.call(window);

// Example mixin usage

class Bar extends ObjectBase {
	public test() {
		
	}
	
	private onDispose(): void {
		alert('object disposed');
	}
	
	static create(): IBar {
		return <any> new Bar();
	}
}
DisposableMixin.addTo(Bar);

// With mixins you need to use interfaces to represent your class
interface IBar extends IDisposable {
	test();
}

var bar = Bar.create();
bar.dispose();

Oct 7, 2012 at 7:55 PM
Edited Oct 7, 2012 at 7:57 PM

Breaking down how the code above works... Mixins & attributes get hung off your class as a static array of objects that will be walked anytime a new instance of your class is created.  If the attribute/mixin implements an onCreated() method it will be called with the instance of the newly created object.  This gives your attribute/mixin an opportunity to do something to the newly created object.  In the CallbacksAttribute what I'm doing is walking the objects methods and binding any method starting with 'cb_' to its 'this' pointer.  Basically you can't call that method accidentally in any other context which is ideal for callbacks. 

For mixins there's an added extend() method.  When you add the mixin to a class the extend() method is called with the classes prototype so that the additional methods can be added to the class.  What's nice about this approach over other mixin system is that we can use TypeScript to strongly type the prototype and we can validate that we're wiring up the right set of extensons.  At create time the mixin acts like an attribute and the mixin can optionally register an onCreated() method that should be called anytime a new instance of the class is created.  This essentially lets the mixin hook into the constructor for every class its attached to. 

Oct 8, 2012 at 8:17 PM

All we need is language level support for this and to have this info available in the AST also, not just runtime. I opened a discussion about this here.

Oct 9, 2012 at 2:01 AM

For mixins, I wasn't crazy about the need to redefine your class as a seperate interface that you then had to pass around everywhere, so I've come up with an alternative approach to describing mixins.  It's still kind of crappy but now slightly less crappy.

In the example below everything is basically the same as my original code but I've changed the may I describe the interfaces for a mixin.  No I'm describing my mixin as all properties, even the methods.  This lets you update your class to say it implements the mixin interface and while you need to add the definitions for all of your mixins properties to your class you don't actually need to provide any implementation.  From TypeScripts perspective, it's fine with the value of these properties being set externally (which is what happens when you add in the mixin) and if you look at the generated code you see that all of these additional property declerations boil out so they have no added overhead.

It sucks that you have to manually declare all of your mixins methods as properties of your class but all things considered this adds zero added overhead to the generated output and seems to be the simplest approach to supporting mixins in the current drop of TypeScript.

 

// Base class & interfaces for supporting attributes & mixins
class ObjectBase {
    constructor () {
        // Invoke attributes
        var _constructor = (<any>this).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                var _attribute = _constructor.__attributes__[i];
                if (_attribute.onCreated) {
                    _attribute.onCreated(this);
                }
            }
        }
    }

    // Static methods

    static attribute(type: new () => ObjectBase, attribute: IAttribute): void {
        if (!(<any>type).__attributes__) {
            (<any>type).__attributes__ = [];
        }
        (<any>type).__attributes__.push(attribute);
    }

    static hasAttribute(obj: any, attribute: new () => IAttribute): bool {
        var _constructor = (<any>obj).constructor;
        if (_constructor.__attributes__) {
            for (var i = 0, l = _constructor.__attributes__.length; i < l; i++) {
                if (_constructor.__attributes__[i] instanceof attribute) {
                    return true;
                }
            }
        }
        return false;
    }

    static mixin(type: new () => ObjectBase, mixin: IMixin): void {
        mixin.extend(type.prototype);
        if (mixin.onCreated) {
            attribute(type, mixin);
        }
    }
}

interface IAttribute {
    onCreated? (obj: any): void;
}

interface IMixin extends IAttribute {
    extend(prototype: any): void;
}

// Example attribute pre-binds callback methods to objects this pointer

class CallbacksAttribute implements IAttribute {
    constructor (private prefix: string) {
    }

    public onCreated(obj: any): void {
        var _constructor = obj.constructor; 
        if (!_constructor.__cb__) { 
            _constructor.__cb__ = {}; 
            for (var m in obj) { 
                var fn = obj[m]; 
                if (typeof fn === 'function' && m.indexOf(this.prefix) == 0) { 
                    _constructor.__cb__[m] = fn;                     
                } 
            } 
        } 
        for (var m in _constructor.__cb__) { 
            (function (m, fn) { 
                obj[m] = function () { 
                    return fn.apply(obj, Array.prototype.slice.call(arguments));                       
                }; 
            })(m, _constructor.__cb__[m]); 
        } 
    }

    static addTo(type: new () => ObjectBase, prefix = 'cb_'): void {
        if (!(prefix in cache)) {
            cache[prefix] = new CallbacksAttribute(prefix);
        }
        ObjectBase.attribute(type, cache[prefix]);
    }
	static cache: { [x: string]: CallbacksAttribute; } = {};
}

// Example mixin makes objects disposable

interface IDisposable {
	dispose: ()=> void;
	isDisposed: () => bool;
}

class DisposableMixin implements IMixin {
    public extend(prototype: IDisposable): void {
        prototype.dispose = function (): void {
            if (!this._isDisposed) {
                this._isDisposed = true;
                if (this.onDispose) {
                    this.onDispose();
                }
            }
        }

        prototype.isDisposed = function (): bool {
            return this._isDisposed;
        }
    }

    public onCreated(obj: any): void {
        obj._isDisposed = false;
    }

    static addTo(type: new () => ObjectBase): void {
        if (!mixin) {
            mixin = new DisposableMixin();
        }
        ObjectBase.mixin(type, mixin);
    }
	static mixin: DisposableMixin = null; 
}


// Example attribute usage

class Foo extends ObjectBase {
	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}
CallbacksAttribute.addTo(Foo);

var foo = new Foo();
foo.cb_test.call(window);

// Example mixin usage

class Bar extends ObjectBase implements IDisposable {
	// IDisposable mixin
	public dispose: () => void;
	public isDisposed: () => bool;

	public test() {
		
	}
	
	private onDispose(): void {
		alert('object disposed');
	}
}
DisposableMixin.addTo(Bar);

var bar = new Bar();
bar.dispose();

 

Oct 9, 2012 at 4:25 AM

Method syntax and property syntax are equivalent in TypeScript. You can replace methods as easily as you can properties. The advantage of property syntax is that you don't need to fake up an implementation and it's obvious that you haven't supplied one when you look at the code, but you don't have to use property syntax if you don't want to.

class methodsCanBeReplaced {
    method() { }
}

methodsCanBeReplaced.prototype.method = () => alert('blah');

var proof = new methodsCanBeReplaced();

proof.method();



Oct 9, 2012 at 5:54 PM
Edited Oct 9, 2012 at 5:54 PM

Yes, either one can be overriden but I was recommending the use of properties because they don't result in any additional JavaScript being generated.  If you use methods you'll have to stub them out with default implementations which will bloat your output code.

Oct 10, 2012 at 3:20 PM

By slightly modifying the addTo methods to return the attribute, we could have the attributes actually specified within the class, which feels cleaner. This does expose the __attributes__ field, though.

class Foo extends ObjectBase {
	static __attributes__ = [ CallbacksAttribute.addTo(Foo) ];
	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}
Oct 10, 2012 at 6:44 PM

If you're going to do that I'd probably do some other simplifications.  I'd call the field 'attributes' instead of '__attributes__', then I'd apply the attribute as a function call, and then eliminate the helper routines from ObjectBase. Here's the revised example:

// Base class & interfaces for supporting attributes & mixins
class ObjectBase {
    constructor () {
        // Invoke attributes
        var _constructor = (<any>this).constructor;
        if (_constructor.attributes) {
            for (var i = 0, l = _constructor.attributes.length; i < l; i++) {
                var _attribute = _constructor.attributes[i];
                if (_attribute.onCreated) {
                    _attribute.onCreated(this);
                }
            }
        }
    }
}

interface IAttribute {
    onCreated? (obj: any): void;
}

// Example attribute pre-binds callback methods to objects this pointer

class CallbacksAttribute implements IAttribute {
    constructor (private prefix: string) {
    }

    public onCreated(obj: any): void {
        var _constructor = obj.constructor; 
        if (!_constructor.__cb__) { 
            _constructor.__cb__ = {}; 
            for (var m in obj) { 
                var fn = obj[m]; 
                if (typeof fn === 'function' && m.indexOf(this.prefix) == 0) { 
                    _constructor.__cb__[m] = fn;                     
                } 
            } 
        } 
        for (var m in _constructor.__cb__) { 
            (function (m, fn) { 
                obj[m] = function () { 
                    return fn.apply(obj, Array.prototype.slice.call(arguments));                       
                }; 
            })(m, _constructor.__cb__[m]); 
        } 
    }
	static cache: { [x: string]: CallbacksAttribute; } = {};
}

function callbacksAttribute(type: new () => ObjectBase, prefix = 'cb_'): IAttribute {
	var cache = CallbacksAttribute.cache;
    if (!(prefix in cache)) {
        cache[prefix] = new CallbacksAttribute(prefix);
    }
	return cache[prefix];
}

// Example mixin makes objects disposable

interface IDisposable {
	dispose: ()=> void;
	isDisposed: () => bool;
}

class DisposableMixin implements IAttribute {
    public extend(prototype: IDisposable): void {
        prototype.dispose = function (): void {
            if (!this._isDisposed) {
                this._isDisposed = true;
                if (this.onDispose) {
                    this.onDispose();
                }
            }
        }

        prototype.isDisposed = function (): bool {
            return this._isDisposed;
        }
    }

    public onCreated(obj: any): void {
        obj._isDisposed = false;
    }

	static cache: DisposableMixin = null; 
}

function disposableMixin(type: new () => ObjectBase): IAttribute {
	var mixin = DisposableMixin.cache;
    if (!mixin) {
        mixin = DisposableMixin.cache = new DisposableMixin();
    }
	mixin.extend(type.prototype);
	return mixin;
}

// Example attribute usage

class Foo extends ObjectBase {
	static attributes = [callbacksAttribute(Foo)];

	private msg = 'callback invoked';
	
	public cb_test(): void {
		alert(this.msg);
	}
}

var foo = new Foo();
foo.cb_test.call(window);

// Example mixin usage

class Bar extends ObjectBase implements IDisposable {
	static attributes = [disposableMixin(Bar)];
	
	// IDisposable mixin
	public dispose: () => void;
	public isDisposed: () => bool;

	public test() {
		
	}
	
	private onDispose(): void {
		alert('object disposed');
	}
}

var bar = new Bar();
bar.dispose();

I like the idea of the attributes being defined at the top of the class but it does come at a cost.  If you look at the generated code for Bar you'll see that your attributes are executed before Bar's methods are added to its prototypes.  That's probably ok but it means the attribute can't reason over the class definition at parse time and instead would have to defer any of that logic to runtime on first create.  Again, that's probably ok.

var Bar = (function (_super) {
    __extends(Bar, _super);
    function Bar() {
        _super.apply(this, arguments);

    }
    Bar.attributes = [
        disposableMixin(Bar)
    ];
    Bar.prototype.test = function () {
    };
    Bar.prototype.onDispose = function () {
        alert('object disposed');
    };
    return Bar;
})(ObjectBase);
Dec 9, 2012 at 3:44 PM

Isn't it a problem that the solution requires a specific base class?

I mean, in practice you could just define the mixins on the base class and be done with it. Am I missing something?

Feb 19, 2013 at 2:55 PM
@mertner Not exactly. If you have many distinct mixins, each doing a completely different thing, by SRP, they should not be re-written as members of the same class. However, they may actually be components useful to one specific class. Mixins allows you to use a form of multiple inheritance to grab many little pieces of functionality, without having to declare them all in one big class that wouldn't make sense. They're very useful for repeated concerns that don't really belong on any one class; things like Authentication, timing, logging, caching, etc...

@ickman This appears to only work when the class including the mixins has a no-parameter constructor - otherwise the compiler blows up. Is this by design? Perhaps I should raise a bug about it?
Jul 13, 2013 at 4:03 AM
Edited Jul 13, 2013 at 4:05 AM
I'd like to revive this thread in order to point out a particularly elegant solution to mixin composition in a statically typed language - Scala traits.
If you aren't familiar with scala traits, here is the start of a series of blog articles covering them, and how they are implemented on the JVM:

http://nurkiewicz.blogspot.com/2013/04/scala-traits-implementation-and.html
http://nurkiewicz.blogspot.no/2013/04/scala-traits-implementation-and_3.html

There are some unique characteristics of scala traits I would like to see brought to TypeScript if Mixins are to be supported.
  • Scala traits are both interfaces and implementation.
  • The code in a trait function is only defined once, not copied into the classes they are mixed into. Classes and instances that mix the trait in forward the calls to the shared implementation.
  • Traits are "stacked" using a process called linearization. This means "super" is predictable and stable at all parts of the stack. It also means that traits are only mixed in once, even if there are multiple references to that trait in the stack.
  • You can mix in traits either when you are declaring a class, or when you are instantiating the class.
While I wouldn't hazard to guess at how traits would be implemented in JavaScript, I think the syntax would look something similar to this, with as few changes to TypeScript syntax as possible:
// TraitProposal.ts
interface IFoo {
  foo: string = "foo";
}
class Base {}
class Sub extends Base implements IFoo {}

var x = new Sub();

 // new 'mix' keyword allows on-the-spot mixin composition
var y = new Base() mix IFoo; // it is REALLY too bad that 'with' is already a JS keyword, or it would be perfect here instead of 'mix'

interface IFoo2 {
  foo2: string = "foo2";
  bar: () => number;
}

var z = new Base() mix IFoo mix IFoo2; // Type error, because bar is not implemented.
Jul 14, 2013 at 10:27 AM
So what happens here

var x = new Sub();
x.foo = "bar";

var y = new Sub();

console.log(y.foo); // presumably this is going to be "bar" because the trait is shared 

Not really seeing how useful a mixin can be if
The code in a trait function is only defined once, not copied into the classes they are mixed into. Classes and instances that mix the trait in forward the calls to the shared implementation.
This sort of behaviour can be better achieved via dependency injection.

A mixin IMO must be copied into the class requiring the mix.

See also: https://typescript.codeplex.com/discussions/406449
Jul 15, 2013 at 2:21 AM
I think I need to clarify. You are correct that a mixin must be copied into the class requiring the mix. That is what happens in scala too. So, in your example, y.foo will be "foo", not "bar". What I meant by "share the same implementation" is that there would be one shared function definition for each class that mixes in the interface. A reference to that function is copied into the class prototype. Obviously this is easier to do in JavaScript than on the JVM, but you get the picture.

However, that's not the whole story. As I said before, Scala uses a linearization algorithm to "stack" mixins together. This is the important part. The linearization algorithm is simple, but it has important qualities:
  1. From any class where you can access "super", the type of "super" never changes. This is critically important and can be broken by naive mixin systems.
  2. For any type, references to an interface's implementated methods only appear once in the entire prototype chain. Part of Scala's linearization process removes duplicates from the stack.
These are hard to visualize, so here's a contrived example:
interface IFoo {
   foo() { return "foo"; }
}

interface IFooExtended extends IFoo {
 foo() { return super.foo() + " from IFooExtended"; }
} 

// methods from IFoo copied to A's prototype
// A's mixin stack: IFoo -> A
class A implements IFoo {
  foo() { return "foo A"; }
}

// methods from IFooExtended copied to B's prototype,
// but NOT methods from IFoo (those were already copied to A).
// B's mixin stack: IFoo -> A -> IFooExtended -> B
class B extends A implements IFooExtended  {}

// methods from IFoo AND IFooExtended are copied into C's prototype
// however, super within IFooExtended.foo refers to the IFoo implementation.
// C's mixin stack: IFoo -> IFooExtended -> C
class C implements IFooExtended() {}

var a = new A();
var b = new B();
var c = new C();

alert(a.foo()); // outputs "foo A"
alert(b.foo()); // outputs "foo A from IFooExtended"
alert(c.foo()); // outputs "foo from IFooExtended"
Oh, and the instantiation time mixin composition is just syntax sugar - it just creates an anonymous class:
interface IFoo {...}
class A {...}
var x = new A() mixin IFoo; // compiler creates an anonymous class: class __anon_A_IFoo extends A implements IFoo {}