Tuesday, July 1, 2014

@MirrorsUsed


Dart mirrors have been trying to distract me from my task of late. Tonight, I finally give into the distraction—if only for a little while.

My task remains to begin to understand what questions I need to ask in order to do a proper job with Design Patterns in Dart. Toward that end, I have been poking and prodding the Factory Method pattern as best I can. This has ranged from the classic subclass approach, to mirrors and benchmarking.

Along the way, I have seen the following more than once:
This import is not annotated with @MirrorsUsed, which may lead to unnecessarily large generated code.
Try adding '@MirrorsUsed(...)' as described at https://goo.gl/Akrrog.
So tonight I see if following this advice does result in smaller code.

I compile my benchmark.dart code to JavaScript with:
$ dart2js -o tool/benchmark.dart.js tool/benchmark.dart
Without the @MirrorsUsed annotation I find that the resulting JavaScript code is very much on the large side:
$ ls -lh tool/benchmark.dart.js                        
-rw-r--r-- 1 chris chris 1.6M Jul  1 22:36 tool/benchmark.dart.js
Let's see if @MirrorsUsed makes a difference. At first I do not read the documentation closely enough, and place the annotation above the class definition. This results in no change in the warnings nor the resulting JavaScript size.

Upon better reading, I realize that I need to place the annotation above the import of the mirrors package:
library factory_method_mirror;

@MirrorsUsed(symbols: 'productMaker', override: '*')
import 'dart:mirrors';

abstract class Creator { /* ... * }
abstract class Product {}
class ConcreteCreator extends Creator { /* ... */ }
class ConcreteProduct extends Product {}
With that, I recompile with the same dart2js command and find:
$ dart2js -o tool/benchmark.dart.js tool/benchmark.dart
$ ls -lh tool/benchmark.dart.js                        
-rw-r--r-- 1 chris chris 171K Jul  1 23:46 tool/benchmark.dart.js
That is a huge improvement.

Unfortunately, my guess at the annotation's arguments appears to have been wrong. My benchmark code no longer runs:
node tool/benchmark.dart.js
Factory Method — Subclass(RunTime): 0.09522241981403347 us.
Factory Method — Map of Factories(RunTime): 4.226399783608331 us.

/home/chris/repos/design-patterns-in-dart/factory_method/tool/benchmark.dart.js:1930
      throw H.wrapException(P.UnsupportedError$("Cannot find class for: " + H.
              ^
Unsupported operation: Cannot find class for: ConcreteProduct0
    at dart.wrapException (/home/chris/repos/design-patterns-in-dart/factory_method/tool/benchmark.dart.js:829:15)
    ...
Unfortunately, I am stumped here.

Eventually, I figure out that I need to supply ConcreteProduct as a target of mirroring. In my full Factory Method mirror implementation, it looks like:
@MirrorsUsed(targets: 'ConcreteProduct')
import 'dart:mirrors';

abstract class Creator {
  Type productClass;
  Product productMaker() {
    return reflectClass(productClass).
      newInstance(const Symbol(''), []).
      reflectee;
  }
}

abstract class Product {}

class ConcreteCreator extends Creator {
  Type productClass = ConcreteProduct;
}

class ConcreteProduct extends Product {}
I can understand why this works and why it is implemented. Even so, it is a bit of a concern.

The target of a @MirrorUsed annotation is how I identify to dart2js that a particular class might be mirrored. As such, even if normal tree shaking does not see this class used, dart2js knows that it still needs to include the class in the resulting output.

The problem that I see is that the implementation that I have been using for the Factory Method is that of a framework. The mirror code (inside the abstract Creator) would reside in the framework package while the concrete implementations would reside in separate codebases. The implication is that my framework has no way of knowing which concrete classes will be defined so it certainly cannot contain a @MirrorsUsed annotation. At the same time, it is the only place mirrors are actually used (the concrete class supplies a type, but does no reflection). So I would seem to have a catch-22 on my hands.

Except that there are other options for @MirrorsUsed() that might help. I will pick back up with them tomorrow. For now, I have working mirror code compiled into much smaller JavaScript. That is enough of a win for one day.


Day #109

2 comments:

  1. I think the best workaround for a lack of a practical newInstance() method is to pass a Function instead of a Type:

    abstract class Creator {
    Function makeProduct;
    }

    class ConcreteCreator extends Creator {
    Function makeProduct = () => new ConcreteProduct();
    }

    Of course then the Creator class is redundant in this case, but it makes sense if you have more metadata.

    To get around the lack of annotations, I've hit on this pattern:

    abstract class Thing {
    MetaThing get meta;
    }

    class MetaThing {
    final Function make;
    const MetaThing(this.make);
    }

    const $Foo = const MetaThing(make: Foo.make);

    class Foo extends Thing {
    ...
    get meta => $Foo;

    static make() => new Foo();
    }

    Then the "metaclass" for Foo is referenced as $Foo, or as foo.meta if you have an instance. More boilerplate than I'd wish, but it avoids using mirrors or transformers.

    ReplyDelete
    Replies
    1. I would argue that newInstance is perfectly practical—as long as speed is not a concern (there are times).

      Regardless, I do like your approach. It is similar to one of the three that I am using in my benchmarks:

      http://japhr.blogspot.com/2014/06/benchmarking-dart2js-code.html

      Both are similar to the “classic” Factory Method pattern. It definitely has the advantage of being quite fast :)

      Delete