Signals, Haxe macros inline dispatch and my turtle "Fabienne"

19.06.2013 3703 4

It's of course about these wonderful events with this so sweet and lightweight synthax :

myAss.addEventListener( AssEvent.POOP, cb_itStinks );
...
myAss.dispatchEvent( new AssEvent( AssEvent.POOP ) );

In fact, as a seasoned coder you are, you know that it is the shortest way to use these events. If we feel enthousiastic we can set others interessting parameters too...:D Well, I will stop joking, I'm not very fair-play because I of course know that the event system is really usefull and powerfull.
I must admit that, as an little, auto didact coder, I used events without knowing about in the past. More precisely a kind of signals, by using simple collections of functions that I call in a loop like that :

class MyAss{
    public var onPoop (default,null) : List<Int->Void>;
    public function new(){
        onPoop = new List();
    }
    public function poop( power : Int ){
        for ( f in onPoop )
            f( power );
    }
}
...
class MyNose{
    public function new(){
        myAss = new Ass();
        myAss.onPoop.add( cb_itStinks );
    }
    function cb_itStinks( power : Int ){
        switch( power ){
            case 1    :
                trace( "Well..." );
            case 2    :
                trace( "Hmm, have you eat something heavy ?" );
            case 3    :
                trace( "You're inhuman !" );
            case 4    :
                trace( "zzzZZZ..." );
        }
    }
}

Here you can see that it's a kind of event system. You can "listen" by pushing your callback function in the List. And you "dispatch" your event by iterating into the List and calling the suscribed functions...
I work for years now with this lightweight event system and I never had any problems or lacks. Of course I hear big mouths telling me that I never worked on big projects that's why I never used events or signals frameworks. I can dispatch, if you're listening to me ;) that you better close your big mouth if you don't want flies get into it :D
Coming back to the subject, let say I want to add a listener that listen just one time. After having received the signal, the function is removed from the collection. Somehting like "addOnce" will be needed to the List, assuming it will not more be just a collection of functions, but instead an object like that :

class SignalSlot<T> {
	public var f	: T;
	public var once	: Bool;
	
	public function new( f : T, once : Bool ) {
		this.f		= f;
		this.once	= once;
	}
}

And the SignalList class can look as it :

class SignalList<T> {
	
    public var slots (default,null)	: Array<SignalSlot<T>>;
   	
    public function new() {
	slots = new Array();
    }
	
    public function add( o : T, once = false ) {
	slots.push( new SignalSlot( o, once ) );
    }
    public function remove( o : T ) {
	var i = 0;
	while ( i < slots.length )
	    if ( Reflect.compareMethods( slots[ i ].f, o ) )
		slots.splice( i, 1 );
	    else
		i++;
    }
    public function exists( o : T ) {
	for ( i in 0...slots.length )
	    if ( Reflect.compareMethods( slots[ i ].f, o ) )
		return true;
	return false;
    }
	
    public function clear() {
	slots = new Array();
    }
}

Ok, we have something funny here but it lacks a dispatch method. We're going to see it later. For now we can do something like that in order to dispatch our signal :

for ( slot in slots ){
    slot.f( power );
    if( slot.once )
        slots.remove( slot )
}

Here we've implemented the concept of an "auto-removable callback" :D
Going this way, we can easily add another feature : event propagation, just by adding a public boolean variable. Let's name it "stopped".
When the SignalList is created, it initialize a new List and set stopped variable to false in its constructor. Then iterating on the collection, we call the functions and it's enough to set the SignalList's public stopped variable to true in one of the callbacks, in order to break, and get outside the loop, like that :

for ( slot in slots ){
    slot.f( power );
    if( slot.once )
        slots.remove( slot )
    if( stopped ) {
        stopped = false;
        break;
    }
}

Now, when a callback function is called, if inside of it, you set your SignalList's public stopped variable to true, the next iteration of the loop will not be called neither the nexts. We've introduced a kind of concept of event propagation, bubbling, or more precisely the capacity to stop the event's propagation !
Ok, you probably think that I feel well with my stuff...It's not totally wrong :D But there is something that is not doable here : implementing a safe-typed dispatch function. Of course, we could write something like that :

public function dispatch( ?args : Array<Dynamic> ){
    for ( slot in slots ){
        if( args == null )
            args = [];
        Reflect.callMethod( null, slot.f, args );
        if( slot.once )
            slots.remove( slot )
        if( stopped ) {
            stopped = false;
            break;
        }
    }
}

But as you should know, using Dynamic and reflection is not a best practice seeing that on the strong typing side. You'll never be warned by the compiler if you pass wrong arguments to the function...
Here come Haxe macros ! With this powerful feature, we can do many things in different ways. Let's see how I decided to turn that for this example (I turned List to Array to optimize performances too...) :

#if haxe3 macro #else @:macro #end
public function dispatch( ethis, args : Array<Expr> ) {
	var pos	= Context.currentPos();
	
	var callerType	= Context.typeExpr( ethis );
	var callerObj	= null;
	var	caller		= null;
	
	switch( callerType.expr ) {
		case TField(e, f)	:
			 callerObj	= e;
			switch( f ) {
				case FStatic(_, ee), FInstance(_, ee), FAnon(ee)	:
					caller	= ee;
				default	:	throw "case not matched";
			}
		default	:	throw "case not matched";
	}
	
	var fullObj	= EField( Context.getTypedExpr( callerObj ), caller.toString() );
	var slots	= { expr : EField( { expr : fullObj, pos : pos }, "slots" ), pos : pos };
	var stopped	= { expr : EField( { expr : fullObj, pos : pos }, "_stopped" ), pos : pos };
	
	// build expressions
	var call 	= {
		expr	: ECall( {
			expr	: (macro f.f).expr,
			pos		: Context.currentPos()
		}, args ),
		pos		: Context.currentPos()
	}
	
	// write inline expression
	return macro {
		var i = 0;
		while ( i < $slots.length ) {
			var f = $slots[ i ];
			$call;
			if ( f.once )
				$slots.splice( i, 1 );
			else
				i++;
			if ( $ethis._stopped ) {
				$ethis._stopped = false;
				break;
			}
		}
	};
}

We can see that this macro function writes inline all the dispatch process, in fact the loop we've seen before. That means that this function doesn't really exists on runtime. It's an a inlined function written while the compilation process. Another thing is that this macro function is a part of the "real" SignalList class. It acts as a member class' function : despite of not being explicitly "static", this functions is a static function working like under using keyword. So when we write :

myAss.onPoop.dispatch( 3 );

First, we can smell that it stinks really strong, and then, that it's the same as we write :

SignalList.dispatch( myAss.onPoop, 3 );

Don't worry, it's a static call only on compile time. On runtime, it's a inlined block of the loop seen before.
You can see and download the SignalList.hx file here.
I've made a benchmark using traditional AVM2 event system, Robert Penner's AS3 Signal lib, the Haxe HXS lib and my Haxe SignalList.
These are my results :

In the left window : Robert Penner's AS3 Signal lib
The middle one : HXS lib
The right window : SignalList

And of course, because you ask yourself what a turtle is doing in my post's title, so here comes a picture of my new friend that I named "Fabienne" :D

Commentaires

05.03.2014 à 10:22 Mark

Hi,
I found this approach very interesting, nice use of macros too I only wonder since the dispatch function is inlined, will filesize get large when you dispatch on 1000 places in your project? Did you also have a benchmark for javascript target?

05.03.2014 à 18:20 Michal

Hi Mark,
Yes, the fact that it's inlined it will grow the filesize, but I don't think you'll have 1000 differents calls
I haven't done any JS benchmark when I wrote that, but just now I tried it and compared to msignal. It seems the SignalList is still faster especially without any listener. I often use the SignalList as it without any problem but I think it needs to be rewritten cleaner.

28.09.2015 à 15:41 有时候

I can't compile them in haxe 3.2

28.09.2015 à 15:44 Michal

Sorry, I haven't compiled it for long time. I wrote that, macros were just implemented in Haxe so you need to adjust with the changes...

Laisser un commentaire

http://
×