Inspired by Juraj's talk, today I wrote a macro to make my PubSub message bus (a favorite little utility of mine) type safe.
Often pubsub APIs are similar to:
publish(channel:String, data:Dynamic):Void
subscribe(channel:String, handler:Function):Void
With perhaps a set of static string channels sitting somewhere:
class MessageTypes {
public static const APP_START:String = "app_start";
public static const APP_RESET:String = "app_reset";
}
The obvious problems: you don't have type safety, and you do have useless verbosity where you don't need it - in the messages class.
This system screams to use enums - the expression of messages is succinct, they can be parameterized and typed. Yes, yes, it all sounds lovely. Here's how it might look:
publish(e:MyEnum):Void
subscribe(e:MyEnum, handler:Function):Void
enum MyEnum {
APP_START(t0:Float); // pass start-up timestamp
APP_RESET;
}
But there's a few catches:
- Every handler then has the signature handler(e:MyEnum) and must switch on the enum to get the data out. Hrmph. Sure, it's only a few lines of code, but it's tedious.
- There's an oddity around identifying an enum instance or an enum constructor -- while the publish function really wants to send an enum instance, the subscribe function wants to identify an enum constructor (technically functions) without specifically generating an instance. But there are no utilities in Type which can identify an enum constructor, only enum instances. (Looping through all enum constructors and testing function equality works in js/neko, but not static cpp.)
This is where my macro comes in. Long story short, the subscribe call is a static macro (but with static extension, it's invoked on a PubSub instance just like a member method.) It replaces the subscribe call with a wrapper which makes the compiler type check the enum constructor function against the handler function:
_pubsub.subscribe(MyEnum.APP_START, handle_start);
Becomes:
_pubsub.dynamic_subscribe('APP_START', function(e) {
switch(e) {
case MyEnum.APP_START(arg1): handle_start(arg1);
default:
throw "Unexpected message: "+e;
}
});
As you can see, the pub sub bus is itself still channel:String / dynamic as it was before, but the enum is passed through the wrapper, and the user's handler function must match the signature of the enum -- in this case handle_start(Float) -- it generates the appropriate number of args for any enum. It also stringify's the enum name -- necessary for the channel parameter under the hood.
Anyway, this was a fun exercise. Thinking of everything as expressions is mind bending, but I could see me getting decent at it. I find Context.parse, Expr.toString(), and all the macro Tools classes incredibly useful.
If you're interesting in more info & code, I could write it up on my blog.
Cheers,
-Jeff
@jeff__ward