CodingTips.new

Here's a couple more neat Haxe tricks.

Article by Dan Korostelev on 2017-03-01.

Comments

CodingTips.new

My previous article was received quite well, so why not have another one? Here's a couple more neat Haxe tricks.

Class constructor closures

It's often desirable to create an instance of a class without actually knowing the exact class name at compile-time.

One common solution is to store references to classes and use Type.createInstance to instantiate them passing an array of arguments. For example:

class Item {
    public function new(id:Int) {}
}

class ItemManager {
    var itemTypes:Map<String,Class<Item>>;

    function registerItemType(name:String, itemClass:Class<Item>) {
        itemTypes[name] = itemClass;
    }

    function createItem(name:String, id:Int) {
        var itemClass = itemTypes[name];
        var item = Type.createInstance(itemClass, [id]);
        return item;
    }
}

This approach has a number of disadvantages, namely:

  • It's not type-safe, meaning that the compiler won't be able to check the number of constructor arguments, or their types, or whether that class even has a constructor.
  • It's not DCE-safe, meaning that the compiler won't know if a class is being used, so in -dce full-mode, it will eliminate your class (or at least its constructor), leading to unexpected run-time issues, unless you explicitly mark your classes with @:keep metadata.
  • It's basically reflection, so it tends to be slower than direct instantiation.

To avoid all these problems, what we really want in a case like this is some kind of "factory". Thanks to Haxe's support for first-class functions, we could represent a factory for our Items as a simple function type:

class Item {
    public function new(id:Int) {}
}

typedef ItemFactory = Int->Item;

class ItemManager {
    var itemTypes:Map<String,ItemFactory>;

    function registerItemType(name:String, factory:ItemFactory) {
        itemTypes[name] = factory;
    }

    function createItem(name:String, id:Int) {
        var factory = itemTypes[name];
        return factory(id);
    }
}

This eliminates all the issues we had with Type.createInstance and also gives us more control over instantiation, since we can now pass any function that matches the required factory signature.

However, instead of just passing a class to registerItemType directly, we now have to pass a function, which is more verbose, for example:

itemManager.registerItemType("myItem", function(id) return new MyItem(id));

Good news: Haxe has a feature for exactly this case. Just type YourClass.new and you'll have a reference to that factory function automatically generated with the same arguments as the constructor:

itemManager.registerItemType("myItem", MyItem.new);

This is concise, type-safe, DCE-friendly and reflection-free at the same time! Moreover, you can use function features like .bind with it.

Immediately-invoked functions

Did you know that the Haxe compiler inlines immediately-invoked anonymous functions? For example:

(function(s) {
    trace(s);
})("Hi");

By looking at the generated JavaScript we can see that the call was inlined to:

console.log("Hi");

Writing code like this doesn't have any sensible use case in Haxe, however knowing this behaviour we can make use of it in conjunction with inline functions.

For example, we could write this naive profiling function:

inline function profile(n, fn) {
    var start = haxe.Timer.stamp();
    for (_ in 0...n)
        fn();
    return (haxe.Timer.stamp() - start) / n;
}

Now if we use it with an anonymous function, like this:

var result = profile(1000, function() {
    trace("hi!");
});
trace(result);

We can look at the generated JavaScript and see that the anonymous function body was inlined at the place of its call:

var start = haxe_Timer.stamp();
var _g1 = 0;
while(_g1 < 1000) {
    ++_g1;
    console.log("hi!");
}
console.log((haxe_Timer.stamp() - start) / 1000);

This can be employed to create nice APIs without runtime overhead. What immediately comes to my mind are all kinds of transactions, map/iter-like functions, controlled internal state exposure and so on. I believe this is gonna be particularily good once arrow function syntax is finally implemented in Haxe.

Fun fact: as described in the first section, the Cls.new syntax generates an anonymous function and if immediately invoked, it will be inlined as well. Whether or not there's a good use case for this is for you to discover. :-)

Avatar for Dan Korostelev

By Dan Korostelev

Published 2017-03-01

 tech