Jeff Ward
Mobile, Web, Linux, and other cool Tech
find me at
Simbulus Consulting Stack Overflow Hacker Rank GitHub LinkedIn Twitter Google Plus

Less Glue via Haxe Macro Lazy Props

Mar 18, 2016 #Haxe#Macros#Development

As programmers, we're well aware of the dangers of singletons and static access. But, lazy programmers (which are the best kind!) will often resort to them because they're so convenient to use! However, through the power of Haxe macros, we can avoid excess glue logic and succinctly declare all our dependencies.

Expediency & Laziness - Noble Goals!

If you've ever protoyped code quickly to meet a deadline or just see if an idea works, you've probably written code that looks like this. I know I certainly have!

class TestCar
{
  public function new()
  {
    start_car();
  }

  private function start_car()
  {
    EngineManager.warmup();
    ClimateControl.set_temp( UserPreferences.last_temperature );
    RadioManager.tune( UserPreferences.last_radio_station );
  }
}

That sure does get us prototyping ideas quickly, but there'll be problems later if this code needs to be long-lived, reusable, and maintainable. There have been scores of articles profering dependency injection techniques that would label the above code as untestable and unmaintainable. But what I want to note is this -- I think a big reason we write code like this is expediency and laziness, which in truth, are quite noble goals.

Moving Toward Testable, Reusable, Maintainable Code

A step in the right direction is to ban static managers, and to declare (thus require) specific dependencies via our constructor. But look how much extra typing is involved:

class TestCar
{
  private var enginerStarter:EngineStarter;
  private var climateControllers:Array;
  private var radioTuner:RadioTuner;
  private var userPreferences:UserPreferences;

  public function new(enginerStarter:EngineStarter,
                      climateControllers:Array,
                      radioTuner:RadioTuner,
                      userPreferences:UserPreferences)
  {
    this.engineStarter = engineStarter;
    this.climateControllers = climateControllers;
    this.radioTuner = radioTuner;
    this.userPreferences = userPreferences;
    start_car();
  }

  private function start_car()
  {
    enginerStarter.warmup();
    for (cc in climateControllers)
      cc.set_temp( userPreferences.last_temperature );
    radioTuner.tune( userPreferences.fav_radio_station );
  }
}

This code is much more testable, dependencies are declared, and for fun we added multi-zone climate control, which is possible now because it's no longer a single static manager. But blast, we had to write (and maintain) a bunch of glue logic -- the dependencies were first declared in the constructor, then basically echoed as variables to hold the references, and then those new variables had to be assigned.

Ugh. This code is more than 50% glue logic, which is slow and (to me) obfuscates the actual application logic. No wonder we (noble lazy coders that we are) often opt for the quick'n'dirty static singleton when prototyping.

Better Code, Without all the Glue Logic

Enter Haxe and its Macro functionality. Haxe macros can modify code and types at compile time. This means they're capable of writing glue logic for us. It's especially tidy when all the important information is neatly in one place (the constructor), and the other glue logic (variable declaration and assignment) is basically repeated info.

The macro I've written for just this purpose is called Lazy Props. It's a work in progress as I'm new to writing macros. You can find it on github at github.com/jcward/lazy-props. Here's the exact same code as above, except using Lazy Props to fill in that glue logic for us:

@:build(LazyProps.build())
class TestCar
{
  @:propPrivate('*')
  public function new(enginerStarter:EngineStarter,
                      climateControllers:Array,
                      radioTuner:RadioTuner,
                      userPreferences:UserPreferences)
  {
    start_car();
  }

  private function start_car()
  {
    enginerStarter.warmup();
    for (cc in climateControllers)
      cc.set_temp( userPreferences.last_temperature );
    radioTuner.tune( userPreferences.fav_radio_station );
  }
}

Ah, that's better. The @:build metadata above the class is a Haxe language construct that tells the compiler to pre-process the class with the given macro. The @:propPrivate('*') metadata is an instruction handled by Lazy Props, telling it to turn all the constructor arguments into private properties. The assignment statements are then automatically prepended into the constructor function.

Now, we might want some of those properties to have different access schema - public, public with getter/setter functions, public read only, etc. That's all possible as well (and via multiple syntaxes as described in the git repo readme). Here's a quick sample where we change the climateControllers and radioTuner to be public read-only:

  @:propPublicReadOnly('climateControllers,radioTuner')
  @:propPrivate('*')
  public function new(...)

Or maybe you're extending a class that already has property declarations for engineStarter and userPreferences, so they can't be redeclared. No problem:

  @:propPrivate('*-engineStarter,userPreferences')
  public function new(...)

You can also get right down and declare the access pattern of a property, if you like, in a similar syntax that Haxe uses (although without type and with shorthand). Here's a public radioTuner that uses both a getter and a setter function:

  @:prop('pub radioTuner(g,s)')
  public function new(...)

And naturally, if you declare a property to use a getter or or setter function (such as like the above radioTuner) you still need to implement those accessor functions yourself:

  public function get_radioTuner():RadioTuner { ... }
  public function set_radioTuner(r:RadioTuner):RadioTuner { ... }

I hope I've sold you on the joys of Haxe macros and less typing! I should also note that there are other solutions in the Haxe ecosystem -- the venerable tink_lang seems to approach the same problem differently (by generating the constructor automatically from the property definitions.)

I'm still working on friendlier error messages and refining rough edges. Feel free to file issues or submit PR's at the repo. Cheers!

comments powered by Disqus