-
Waking up to haXe Macros
I’ve been a bit slow to get into haXe macros but i found a use case that I wasn’t able to solve to my satisfaction any other way.
I blogged previously about a Part - a self contained component. A Part knows how to start/stop itself, can signal errors and exceptions and has it’s own set of events.
I wanted to combine Parts into Assemblies, where an Assembly knows what it’s parts are and contains the logic that specifies how these Parts interact, via the events the parts emit. An assembly is also a Part. So an Assembly is a typical collection class, which you can iterate over.
To retain typing when dealing with an Assembly and it’s constituent Parts one needs to use an Enum as a container, e.g. to specify an Assembly and it’s Parts I do this.
enum ViewParts {
LOGINVIEW(loginView:LoginView);
REGISTERVIEW(registerView:RegisterView);
LAUNCHVIEW(launchView:LaunchView);
BASEVIEW(baseView:BaseView);
CONTROLLER(controller:Controller);
}_viewAsm = new ViewAssembly();
_viewAsm.add(REGISTERVIEW(new RegisterView(“#pageView”))); _viewAsm.add(LOGINVIEW(newLoginView(“#pageView”)));
_viewAsm.add(LAUNCHVIEW(new LaunchView(“#pageView”)));
_viewAsm.add(BASEVIEW(new BaseView()));
_viewAsm.add(CONTROLLER(new Controller()));The whole point of an assembly is to provide controlling logic for the constituent parts, but the problem _was_ that adding the view to the collection as an enum greatly inhibits the ease at which you can get at the objects you want to manipulate, i.e. you need to use a switch to get at them.
So with a simple macro I’ve been able to remove this limitation. Below is the controller code from within the _viewAsm assembly which switches between the views, I’ve only included two cases for brevity. The main point to note is that I can access my views by name; loginView, registerView, controller etc. These are fully typed, no manual var declarations, no untyped string switches in sight, or unnecessary switch dereference to get an object from it’s container!
watch(function(vp,state) {
switch(vp) {
case LOGINVIEW(lv):
switch(state) {
case Started:
case Stopped:case Error(s):
case Except(ex):
ex.toString().info();
case Event(s):
switch(s) {
case onLogin(email,passwd):
controller.doLogin(email,passwd);
case onRegisterView:
lv.stop();
registerView.start();
}
}case REGISTERVIEW(rv):
switch(state) {
case Started:
rv.observe(function(op) {
switch(op) {
case onRegister(email,password,name):
controller.doRegister(email,password,name);
case onLoginView:
registerView.stop();
loginView.start();
}
});
default:
}
});The Assembly class has a macro that adds for each constructor of the enum a new field of the correct type into the Assembly class, thus all the parts added may be accessed internally or externally by name. The Assembly, in a way, implements a Dynamic<ViewParts>.
The new assembly field name is created by the macro from the enum by extracting the var name, e.g. loginView from the constructor
LOGINVIEW(loginView:LoginView);
and adding it as haXe meta data to the enum. At runtime the meta data is extracted and applied as each new enum constructor is added.
What’s great with this is I get single instance dependency injection to boot!
Here’s how I attach the macro to the Assembly class (which ViewAssembly derives from)
@:autoBuild(cloudshift.core.PartAssemblyMacro.build())
class AssemblyImpl<T,Q> implements PartAssembly<T,Q>, implements Part<Q>{}And here’s the macro
class PartAssemblyMacro {
@:macro public static function build() : Array<Field> {
var pos = haxe.macro.Context.currentPos();
var fields = haxe.macro.Context.getBuildFields();
var local = haxe.macro.Context.getLocalClass().get();switch(local.superClass.params[0]) {
case TEnum(t,params):
var en = t.get();
for (n in en.names) {
var enumFld = en.constructs.get(n);
switch(enumFld.type) {
case TFun(args,ret):
for (a in args) {
var thispos = haxe.macro.Context.currentPos();
enumFld.meta.add(“woot”,[{ expr : EConst(CString(a.name)), pos : thispos }],thispos);
switch(a.t) {
case TInst(ref,prms):
var classDef = ref.get();
var myt = TPath({ pack : [], name : classDef.name, params : [], sub : null });
fields.push({ name : a.name, doc : null, meta : [], access : [APublic], kind : FVar(myt,null), pos : pos });
default:
}
}
default:
}
}
default:
}
return fields;
}
}I haven’t included the runtime where I decode the metadata and add the field into the assembly instance but it’s trivial.
To sum up, I’m blown away by the possibilies now that I’ve woken up to haXe macros.
My Part/Assembly system will be posted on Github after a bit more testing as part of the https://github.com/cloudshift/Core
-
Unobtrusive Class Extensions
As a framework developer, one of my concerns is how to introduce functionality with as little disruption to the user of the framework as possible.
For example, inheritance as the primary mechanism of extension is used as the first resort for most users, hence if the framework forces the use of it’s base classes it becomes a larger question if it will be used.
Let me explain my problem. I want to integrate a component model into an application, I want to be able to start/stop components, to query their state, to observe them using an Observer pattern. But I don’t want to introduce a special base class to do this, this enables my component model to be more readily adopted.
One can implement an interface, but if your extension requires state, and has many methods it’s fairly intrusive.
One can use “using” to mixin static methods, but as a framework developer your mixin methods will not be tied to a user’s class but something more generic.
So the solution is to use a combination of the two methods above. In the code my component is called a Part, so I want to be able to mixin Part functionality with minimal intrusion to a user class, so I defined a Part interface with a single var.
interface Part {
var part_:Part_;
}To use my component model the user must implement Part, to do so they have to use the real Part_ implementation class.
class MyClass extends MyBase , implements Part {
public var part_:Part_;
public function new() {
part_ = new Part_();
}public function start() {
part_.notify(“started”);
}}
The user requires to create a part_ var and initialize it - the part_ var provides any plumbing the extension requires within the class, e.g. in my Part I initialize an observable and have other functionality.
When one uses the class, you want the Part functionality without accessing part_, and this is where we can use “using” to make the extension as seamless as possible.
class PartExtensions {
public static var observe(Part:p,cb:Dynamic->Void){
p.part_.events.observe(cb);
}}
Finally
import MyClass;
using PartExtensions;var inst = new MyClass() ;
….
inst.observe(function(msg) { });
For two lines of code to get started the user can get full integration with Part extensions without having to rethink their class hierarchy. That’s pretty good without macros.