Search code examples
inversion-of-controlhaxe

Using Class<T> as a Map key in Haxe


I'd like to store instances of models in a common provider using their classes or interfaces as a keys and then pop them up by class references. I have written some code:

class Provider {

    public function new() { }

    public function set<T:Any>(instance:T, ?type:Class<T>) {
        if (type == null)
            type = Type.getClass(instance);
        if (type != null && instance != null)
            map.set(type, instance);
    }

    public function get<T:Any>(type:Class<T>):Null<T> {
        return cast map.get(type);
    }

    var map = new Map<Class<Any>, Any>();

}

...alas, it's even doesn't compile. Probably I have to use qualified class name as a key rather than class/interface reference? But I'd like to keep neat get function design that takes type as argument and returns object just of type taken, without additional type casting.

Is it possible or should I change my approach to this problem?


Solution

  • The issue of using Class<T> as a Map key come up every so often, here is a related discussion. The naive approach of Map<Class<T>, T> fails to compile with something like this:

    Abstract haxe.ds.Map has no @:to function that accepts haxe.IMap<Class<Main.T>, Main.T>`

    There's several different approaches to this problem:

    • One can use Type reflection to obtain the fully qualified name of a class instance, and then use that as a key in a Map<String, T>:

      var map = new Map<String, Any>();
      var name = Type.getClassName(Main);
      map[name] = value;
      

      For convenience, you would probably want to have a wrapper that does this for you, such as this ClassMap implementation.


    • A simpler solution is to simply "trick" Haxe into compiling it by using an empty structure type ({}) as the key type. This causes ObjectMap to be chosen as the underlying map implementation.

      var map = new Map<{}, Any>();
      map[Main] = value;
      

      However, that allows you to use things as keys that are not of type Class<T>, such as:

      map[{foo: "bar"}] = value;
      

    • The type safety issues of the previous approach can be remedied by using this ClassKey abstract:

      @:coreType abstract ClassKey from Class<Dynamic> to {} {}
      

      This still uses ObjectMap as the underlying map implementation due to the to {} implicit cast. However, using a structure as a key now fails at compile time:

      var map = new Map<ClassKey, Any>();
      map[{foo: "bar"}] = value; // No @:arrayAccess function accepts arguments [...]