8 things I don’t like in Haxe

STOP right here. Before I say a few words of criticism about Haxe, you should know, that i REALLY like this language.

Btw, this post is pretty old, haxe was improved since I wrote it.
Feel free to read other posts 🙂
Top 3 (in my taste):

Ok, let’s continue.

I use many programming languages everyday, and Ruby Haxe is the most promising of them. Thanks to Haxe I can build my projects for web, for mobiles, build them as native applications. Comparing with JavaScript, Haxe has strict typing; comparing with C++, Haxe is MUCH easier to write. Enum-matching is fantastic thing! And last, but not least, Haxe allow me to write platform-specific code via untyped keyword (just like old good #ifdef in C / C++).

But there are several things that make programming in Haxe more complex and not so exciting. Maybe some of that things will be fixed in next versions of Haxe, while other things is by design and will not be fixed even in Haxe 4 / Haxe 5. Anyway, I still want to talk about them.

 

1. Inconsistent behavior while using uninitialized class variables

Suppose, we have following code:

class Test {
    private static var testvar:Int;

    public static function main() {
        trace(testvar + 1);
    }
}

We’ve got exception on neko, NaN on javascript and 1 on c++.

Ok, let’s change code a bit:

class Test {
    private static var testvar:Null;

    public static function main() {
        trace(testvar);
    }
}

Now we got null on neko, undefined on javascript and segmentation fault on c++.

Obliviously, first and second code examples are TOTALLY WRONG. But all I want is more consistent behavior on different targets, or (much better) compiler warning about using an uninitialized variable.

Resolution: nice feature to have.

 

2. Float is “stronger” than Int

Consider the following code:

class Test {
    private static function foo():Int {
        return 5;
    }

    private static function bar():Int {
        return 2;
    }

    public static function main() {
        var a:Int = Std.int(foo() / bar());
        trace(a);
    }
}

We can’t just write

var a:Int = foo() / bar();

because Float is result on integer division. While I totally agree, that mathematically it can be Float only. But we are programmers, not mathematicians. Sometimes it is very useful to be able to divide two integers, and get integer, and do it fast (it can be handy while writing computer emulators, various image manipulations, etc.).

When you use Std.int(foo() / bar()) it compiles into

int _g = ::H03_obj::foo(); // nice
int _g1 = ::H03_obj::bar(); // still nice
Float _g2 = (Float(_g) / Float(_g1)); // noooooooo
int a = ::Std_obj::_int(_g2); // noooooooo #2

Maybe c++ compiler will be smart enough to optimize it as integer division (or even compute constant at compile time), but I think that Haxe c++ backend can be smarter, and optimize such cases.

Resolution: must have.

 

3. Absence of abstract class methods (like abstracts in java / php or virtual methods = 0 in c++)

Haxe have abstracts, but it is Haxe own unique feature, while in other languages abstract class it class with some methods, that you must override when extending that class.

I even prepare some code examples, but you know… while I playing with snow / flow, I found old-school abstract classes in Haxe in snow.utils.AbstractClass. Nuff said

macros

Resolution: already done 🙂 , but nice to have it in Haxe core.

Update: as of 2015-04-18 there is no more snow.utils.AbstractClass, so I updated link, and currently it points to gist.

 

4. Inconsistent ObjectMap behaviour

Let’s say you have following code:

class Test {
    public static function foo():Void {
        trace("foo");
    }

    public static function main() {
        var map = new ObjectMap<Dynamic, String>();
        map.set(foo, "FOO");

        if (map.exists(foo)) {
            trace("exists");
        } else {
            trace("not exists");
        }
    }
}

It works fine on flash and javascript targets, but fails with exception on neko and didn’t work as expected on c++.

Instead of simple and nice ObjectMap you must create some custom data structure, and use Reflect.compareMethods() to compare methods consistently across platforms.

Resolution: want.

 

5. Inconsistent @:generic behavior

Want more tricks with functions?

class Test {
    static function foo<T>(t:T):Void {
        trace("Haxe is great!");
    }

    static function bar():Void {
    }

    static function main() {
        foo(bar);
    }
}

It works just fine, and you, for some reason, want to speed-up this app and generalize function foo:

class Test {
    @:generic
    static function foo<T>(t:T):Void {
        trace("Haxe is great!");
    }

    static function bar():Void {
    }

    static function main() {
        foo(bar);
    }
}

Ooops! Now you get a compiler error

Type parameter must be a class or enum instance (found Void -> Void)

Resolution: why not? Nice to have.

 

6. Not optimized iteration over map keys

Every time when you write something like

for (key in map.keys()) {
  ...
}

Haxe copy keys to temporary array and iterate over it (at least for javascript and c++)

var $it0 = map.keys();

while ($it0.hasNext()) {
    var key = $it0.next();
    ...
}

...

haxe.ds.StringMap.prototype = {
    ...

    keys: function() {
        var a = [];

        for (var key in this.h) {
            if (this.h.hasOwnProperty(key)) {
                a.push(key.substr(1));
            }
        }

        return HxOverrides.iter(a);
    }
}

This is simple, and work fine, but I want something like

for (var $key0 in map.h) {
    if (map.h.hasOwnProperty($key0)) {
        var key = $key0.substr(1);
        ...
    }
}

I know, some map improvements already added to Haxe recently, but I want more improvements 🙂

Resolution: must have.

 

7. Tricky unsafe cast syntax (compared to safe cast)

Let’s say you have function findViewById() that returns object of class View, but you know that in that place it is safe to upcast it to class TextView and call function setText().

Nice and simple with safe cast:

cast(findViewById(R.id.myTextView), TextView).setText("Cool");

And long time I thought that if you want to use unsafe cast, you must create additional variable:

var myTextView:TextView = cast findViewById(R.id.myTextView);
myTextView.setText("Cool");

I dreamed of something like ucast(). But I was wrong, actually you can do it with unsafe cast:

(cast findViewById(R.id.myTextView):TextView).setText("Cool");

Resolution: already in Haxe, but tricky.

Update: and it didn’t work on cpp 🙁

Update 2: finally works in Haxe 3.2

 

8. Must write a lot of code when comparing enum by constructor

Let’s have following enum:

enum Foo {
    Bar(i:Int);
    Baz(s:String);
}

For example, I want to do something only when variable f or type Foo was created with Foo.Bar constructor.

a) I can use enum matching:

switch (f) {
    case Foo.Bar(_): trace("here 1");
    default:
}

Lot of code, but effective – number comparison for javascript target:

switch (f[1]) {
    case 0:
        console.log("here 1");
        break;

    default:
}

b) I can use Type.enumConstructor():

if (Type.enumConstructor(f) == Type.enumConstructor(Foo.Bar(0))) {
    trace("here 2");
}

Too lot of code, and not effective – enum creation + function call + string comparison for javascript target:

if (f[0] == Type.enumConstructor(Foo.Bar(0))) {
    console.log("here 2");
}

// for javascript target enum is an array, where first element (with index 0) is enum constructor name, second element (with index 1) is enum constructor index and other elements is enum constructor arguments
// for example Foo.Bar(42) will be ["Bar", 0, 42], and Foo.Baz("qq") will be ["Baz", 1, "qq"]

c) I can use Type.enumIndex():

if (Type.enumIndex(f) == Type.enumIndex(Foo.Bar(0))) {
    trace("here 3");
}

Still lot of code, but super effective – direct number comparison for javascript target:

if (f[1] == 0) {
    trace("here 3")
}

What I want:

if (f == Foo.Bar(_)) {
    ...
}

Resolution: not necessary, but nice feature to have.

Update: actually, it is already in haxe – EnumValueTools.match()

 

Conclusion

For me Haxe is something like One Ring from Hobbit – One Language to unify them all. And it would be nice, if you could write code without thinking about all-these-platform-specific things. Maybe in Haxe 4?

11 thoughts on “8 things I don’t like in Haxe

    • Thanks! I’m still learning haxe, so I don’t know about EnumValueTools.match(). It is really great method!

  1. Regarding #7, could you just do `static inline function as(v:Dynamic, t:Class):T return cast v;`? Usage is pretty simple (`as(some, Test)`) and it doesn’t look like it could break on some target.

    • I can’t see your example, because try.haxe.org is currently down, so I’ll try to create such function by myself.

      Say, we have following classes:

      class A { public function new() {} }
      class B extends A { public function foo():Void { trace("foo"); } }
      class Test { public static function main() { bar(new B()); } ............ }

      #1 – your approach

      static inline function as(v:Dynamic, t:Class):T return cast v;
      private static function bar(a:A):Void { as(a, B).foo(); }

      -> Haxe compiler error: “Class not found : T”

      #2 – I borrow and modify this code from openfl.Lib.as()

      private static inline function ucast1<T>(v:Dynamic, c:Class<T>):T { return cast v; }
      private static function bar(a:A):Void { ucast1(a, B).foo(); }

      -> generates following c++ code

      Void Test_obj::bar(::A a) { a->foo(); return null(); }

      -> c++ compiler error: “no member named ‘foo’ in ‘A_obj'”

      #3 – try to remove “inline”

      private static function ucast2<T>(v:Dynamic, c:Class<T>):T { return cast v; }
      private static function bar(a:A):Void { ucast2(a, B).foo(); }

      -> generates following c++ code

      Void Test_obj::bar(::A a) {
      ::Test_obj::ucast(a, hx::ClassOf<::B>())->__Field(HX_CSTRING("foo"), true)();
      return null();
      }

      -> works, but not what I want and it is slow

      #4 – just regular cast

      private static function bar(a:A):Void { cast(a, B).foo(); }

      -> generates following c++ code

      Void Test_obj::bar(::A a) { (hx::TCast<B>::cast(a))->foo(); return null(); }

      -> works, but slower than unsafe cast

      #5 – by the way, what code will be generated by unsafe cast?

      private static function bar(a:A):Void { var b:B = cast a; b.foo(); }

      -> generates following c++ code

      Void Test_obj::bar(::A a) { ::B b = a; b->foo(); return null(); }

      -> nice, super fast, but must write lot of haxe code

      #6 – what I want

      private static function bar(a:A):Void { ucast(a, B).foo(); }

      -> should generate following c++ code

      Void Test_obj::bar(::A a) { ((::B)a)->foo(); return null(); }

      -> 🙁 currently not possible

  2. The 1st thing I need in haxe is the Simple Data Type.

    The Bool, Int [Int16, Int32, Int64], Float [Float32, Float64] DataType to be Object Type and the bool, int [int16, int32, int64], float [float32, float64] to be Object type (same as the java)

    and we get the different things in the swap functions

    -> in the int case
    swap(a: int, b: int ) : void {
    var c:int = a;
    a = b;
    b = c;
    }

    vat a:int = 20;
    var b:int = 30;
    swap(a, b);
    // now a = 20 and b = 30

    -> and in the Int case
    swap(a: Int, b: Int ) : void {
    var c:Int = a;
    a = b;
    b = c;
    }

    var a:Int = new Int(20);
    // var a:Int = 20 ; // OK
    var b:Int = new Int(30);
    // var b:Int = 30 ; // OK
    swap(a, b);
    // now a = 30 and b = 20

    • This will not work in java either:

      public static void swap(Integer a, Integer b) {
          Integer c = a;
          a = b;
          b = c;
      }
      

      you just swap pointers inside swap function, but outside this function a and b will remain unchanged.

      If you really need swap function in Haxe, I think you can use some macro magic to do it 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *

*