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

Haxe Notes For ECMA Coders: Object Literal Notation

May 16, 2015 #Development#Haxe#OpenFL#JavaScript

ECMAScript programmers (JavaScript, ActionScript, etc) are familiar with writing object literals in JSON notion. This appears to work in Haxe, but you'll quickly run into pitfalls and type issues. A bit of background about the Haxe type systm will help you avoid a lot of frustration.

Foreword: This is the first in hopefully a series of posts reviewing the nicities and pitfalls of the Haxe language from the perspective of an ECMAScript (aka JavaScript and ActionScript) hacker.

Quick hacking with JSON literals

In languages like JS and AS3, you can quickly prototype code with Objects that look like this:

var config = {
  name:"foo test",
  values:[1.2, 3.5]
}

In ECMAScript, we think of config simply as an Object with String keys and values of any type. And it's dynamic - we can always add or delete more keys and values (in Haxe, Dynamic means something entirely different, but we'll get to that later.)

So I've heard Haxe is like JS/AS3, right?

TLDR; Be careful with this notion, it's like a distant cousin, twice removed.

If you're just learning Haxe, you may have heard that it's very similar to JS or AS3, and you might write JSON literal objects like the above. It would compile and appear to work, as you can see here:

var config = {
  name:"foo test",
  values:[1.2, 3.5]
}
trace(config.name);
trace(config.values);
Run this code try.haxe.org/CF27C

But beware -- the object you've created with the same JSON notation you'd use in JS/AS3 is actually quite different from the one you'd get in those languages, and if you don't understand why it can be quite frustrating. How so? Read on...

The first thing you might notice is that config is not a dynamic object. You try to add a key/value pair to it, and you get a compiler error:

var config = {
  name:"foo test",
  values:[1.2, 3.5]
}
config.timestamp = 1431797453;
Build failure
Test.hx:7: characters 2-18 : { values : Array, name : String } has no field timestamp
Run this code try.haxe.org/47240

Wow, that's a confusing error message for a newcomer... until you understand how Haxe interpreted your config object. More on that later; you just wanna get something working so you impatiently plow forward...

Impatiently turning to Dynamic

TLDR; Don't do this until you understand the type system.

So being an ECMA-minded hacker, you may search for a dynamic JSON-like object and find the Dynamic class. It seems to do what you want, supporting dot notation and adding more key/value pairs:

var config:Dynamic = {
name:"foo test",
  values:[1.2, 3.5]
}
config.timestamp = 1431797453;
trace(config.name);
trace(config.values);
trace(config.timestamp);
Run this code try.haxe.org/6C7d1

But you will soon realize the drawbacks of this approach when you try to iterate over your object, or access with array notation (e.g. config[key]). You try a bunch of different things you'd do in ECMA-land, until you eventually find the Reflect API, and 4 hours later you have code that doesn't perform well and is an ugly mess:

var config:Dynamic = {
var config:Dynamic = {
name:"foo test",
values:[1.2, 3.5]
}
config.timestamp = 1431797453;

// Failed attempts to iterate:

// for (key in config) { }
// Err: You can't iterate on a Dynamic value...

// var keys = config.keys();
// Err: Uncaught TypeError: undefined is not a function

// Arg, this is ugly and verbose!
for (key in Reflect.fields(config)) {

  // trace("config."+key+" = "+config[key]);
  // Err: String should be Int

  trace("config."+key+" = "+Reflect.field(config, key));
}
Run this code try.haxe.org/37A3f

"If it was this hard to instantiate, iterate, and access an object," you think to yourself, "I can't imagine doing anything serious in Haxe." You give up on Haxe and curse the name of Nicolas Cannasse.

Side note: Actually, there are two Haxe compiler improvements that vould make this much more tenable -- I'll make sure to voice these ideas:

  1. Make the Array get/set accessor on Dynamic a shortcut for the Reflect.field get/set, and
  2. Make iterating over a Dynamic infer Reflect.fields call (unfortunately this is opposite behavior to Map iteration, which defaults to iterating over values -- yeesh.)

Then you'd just be able to access Dynamics easily like in ECMAScript, and Haxe already provides lots of ease of use features.

Understand Haxe Types and Literal Notation

Ok, slow down, don't give up yet. First understand that Haxe isn't ECMAScript, and its type system is actually much more nuanced and powerful. This is good as it allows for better performing code, and sometimes it helpfully infers this automatically... But as with all things powerful and complex, there are quite a few pitfalls to avoid.

If you're wiser and less lazy than I am, you'll read the Haxe documentation on Types and the Type System before jumping in -- maybe read it twice, and you'll discover some really useful and time-saving stuff:

JSON-like literals become anonymous structures

For starters - you thought you specified a ECMA-ish dynamic Object with JSON:

var config = {
  name:"foo test",
  values:[1.2, 3.5]
}

But in fact, due to the sluggish performance of dynamic objects, Haxe infers an anonymous structure with two keys, name and values. So that compiler error above starts to make more sense:

Build failure
Test.hx:7: characters 2-18 : { values : Array, name : String } has no field timestamp

You can't dynamically add keys to such a structure. (This is a trafe-off so the Haxe compiler can optimize access to that object.)

Meet Map, a more performant data structure

You can also initialize your object as a Map. This is an optimized hash object that has specific types for its key and value, but to most closely match your JSON Object, you could use Map:

static function main() {
  var config:Map<String, Dynamic> = [
    "name"=>"foo test",
    "values"=>[1.2, 3.5]
  ];
  config.set("timestamp", 1431797453);
  trace(config);
  for (key in config.keys()) {
    trace("config."+key+" = "+config.get(key));
  }
}
Run this code try.haxe.org/4d150

A few things to note:

AS3 programmers: Note that iterating over the Map itself ala for (key in config) iterates over the values -- which is the behavior of for each in AS3. See try.haxe.org/#6B474

So Map<String, Dynamic> is perhaps most like Objects in JS/AS3.

But I just want a type that behaves like an ECMA Object

Indeed. Maps are great, but if you start nesting objects, without the dot or array access notion, things start to get more hairy. You start to see why EMCA dot accessors and array accessors make working with objects so quick and easy.

The good news is, Haxe has some incredibly power language features -- such as macros, abstract classes, and the ability to define array access -- that can help. I found a great post (can't locate it just now) about an Abstract Object class that gets pretty close to EMCA Objects, and I'm playing with the notion here: try.haxe.org/#F1bA1

Let me know in the comments if you know of related work.

More to come!

I plan to add more posts about Haxe type checking and type inference from the perspective of a coder familiar with JavaScript or AS3. Check back or watch my twitter feed for that!

comments powered by Disqus