Brief Haxe Tutorial

John Gabriele

2019-09

This is a concise, fast-paced, limited tutorial covering just enough Haxe to get you productive right away.

Haxe is a modern, general-purpose, high-level, object-oriented programming language with a familiar syntax. It can be compiled to native, to VM bytecode (its own HashLink VM, or to the JVM), to numerous target programming languages1, or can run code with its own built-in interpreter.

For a short sample of some Haxe code, visit the haxe.org website and scroll down.

It’s assumed here that you’re already familiar with some other JavaScript-/Java-/ActionScript3-style language.

In the text below where I write, “${feature} acts just as you’d expect it ought to”, that’s intended to mean that Haxe does just what you’d think a sensible, block-structured, lexically-scoped, modern language that obeys the principle of least astonishment would do. ;)

This document covers the soon-to-be-released Haxe version 4. I’m using version 4.0.0-rc.2.

Language Overview

Some characteristics of Haxe:

See also the official Language Introduction.

Pros and Cons of Haxe

Pros

Cons

Languages Comparable to Haxe

Haxe first appeared in 2006. The following are a handful languages (roughly in order by date-first-appeared) that are comparable to Haxe (that is, being statically-typed, GC’d, OO, “application-level”, modern languages with familiar syntax):

Language First Appeared License Comments
Dart 2011 BSD From Google
Kotlin 2011 Apache 2.0 From JetBrains
TypeScript 2012 Apache 2.0 From Microsoft
Swift 2014 Apache 2.0 From Apple
ReasonML 2016 MIT From Facebook

Two observations:

Setup for this Tutorial

This tutorial was written using Haxe 4 on GNU/Linux, targetting haxe’s built-in interpreter. To install Haxe see my getting started doc.

To get set up targetting Haxe’s own HashLink VM, see my getting started with HashLink doc. If you’d like to target some other environment, visit the Haxe introduction and follow the link buttons under “Setup your development environment”.

For the rest of this tutorial, create your own project directory containing a ./src/Main.hx file:

class Main {
    public static function main() {
        trace("Hello, World!");
    }
}

and an ./interp.hxml file (described in a moment):

-p src
-m Main
--interp

Run your code on the command line from the project directory like so:

haxe interp.hxml

Note that when not using interp (Haxe’s built-in interpreter), you’d usually have to type two separate commands:

  1. a haxe invocation to compile/build your program, and then
  2. a target-specific command to run or otherwise use the output that haxe just produced.

hxml files

For invoking the haxe command, rather than type out the necessary command line options every time, you can instead put those options into a .hxml (“hax em ell”) file. For example, instead of typing:

haxe -p src -m Main --some-target whatev

you can instead create an .hxml file containing those options, and then type haxe some-target.hxml to get the same result.

If you build your project for multiple different targets, you’ll probably have multiple hxml files — one for each target (ex., interp.hxml, hl.hxml, lua.hxml, etc.), or maybe one for each domain (ex., desktop.hxml, mobile.hxml, web.hxml).

hxml files can contain comments, which start with #.

The Language

Basics

// single-line comment

/*
A multi-line
comment.
*/

/**
  A multi-line doc comment.
  Indent however you like. You can close
  with two stars before the slash if that
  looks nicer to you.
*/

Haxe is not whitespace-sensitive.

Haxe is case-sensitive. Variable names are often writtenCamelCase, with types NamedLikeThis.

null is used to indicate when a variable refers to no object.

Void is used to indicate the absence of type, or that something has no value (ex. when used as the return type of a function).

Basic Types

The so-called “basic types” are Bool, Int, and Float:

true, false
1, 0xffff
1.2, -3.4, 1e3

On HashLink, Ints are 32-bit signed (so, -2,147,483,648 → 2,147,483,647, and they wrap), and Floats are 64-bit.

Strings

Strings are immutable and Unicode. Not a “basic type”, but there’s a built-in literal syntax for strings, and string interpolation is supported:

"a string"                                   // double-quoted
'a string that allows ${foo} interpolation'  // single-quoted

"concat" + "enate strings"

"both single- and double-quoted strings
can be written across multiple lines and
can contain \t and \n."

See the String API docs for full list of String fields (ex. length, charAt(), charCodeAt(), indexOf(), split(), …).

Variables and Constants

You use var or final to define variables. Thanks to type inferencing, you don’t often need to explicitly specify the type:

var   x = 3.2;
final n = 160;  // A constant.

final variables can’t be assigned to another object, but they can refer to a mutable object (which could itself be modified).

On HashLink (and other statically-typed targets):

var x = 5;
x = null;  // ERROR, `null` can't be used as a basic type.

When types cannot be inferred you must include them explicitly. Type names come after the variable name and colon, for example:

var x : Int = 5;
// optionally, omit the extra spaces
var x:Int = 5;

To see (at compile-time) the type of any variable, put in $type(x); which will print out a warning line to the console indicating x’s type.

Classes

A class defines a new type, and typically goes into its own like-named file.

Classes have fields which may be variable fields or function fields (aka methods). Fields also may be member (aka, non-static) or static. Summing that up:

  instance static
variable: member variable static variable
function: member method static method

Note the terminology: class fields that are functions (that is, functions belonging to a class) are referred to as “methods”. In Haxe you can create local functions as well, but those aren’t considered methods.

All fields are private and member by default.

As an example, from your project directory make a src/my_pkg/MyClass.hx file:

package my_pkg;

// Classes are public by default.
class MyClass {
    var memberVariable = 3; // private member variable

    static var staticVariable = "yo";

    // Constructor must be public.
    public function new(n) {
        trace("Hi from MyClass ctor!", n);
        this.memberVariable = n;
    }

    public function incr() {
        trace("Incrementing our member variable.");
        this.memberVariable++;
    }

    public function show() {
        trace("member variable's value:", this.memberVariable);
        trace("static variable's value:", staticVariable);
    }

    public static function staticMethod() {
        trace("Hi from MyClass.staticMethod()!");
    }
}

and use it from Main.hx:

import my_pkg.MyClass;

class Main {
    public static function main() {
        MyClass.staticMethod();
        var mc = new MyClass(9);
        mc.incr();
        mc.show();
    }
}

Functions

In Haxe, functions are defined within classes (or within other functions). You’ve seen examples above.

// Function args can have default values. Here, `x` is an Int. On
// static platforms (like HashLink), you may not pass in `null`
// as a value for `x`.
public function foo(x     = 7) {...}
public function foo(x:Int = 7) {...} // same

// Function args can be made optional. If omitted, gets assigned
// `null` (`Null<Int>` on static platforms).
public function foo(?x : Int) {...}

// Optional function arg with default value. You can pass in `null`
// on static platforms (`x` is `Null<Int>` "nullable").
public function foo(?x = 7) {...}

When necessary you can explicitly specify return type:

function foo()      : String     {...}
function bar(x:Int) : Array<Int> {...}

You return values with a return statement.

Regarding Copying

var x = 5;
var y = x;
y++;       // `y` is now 6
trace(x);  // `x` is still 5
// Similarly for Strings, as they're immutable.

// But for other objects:
var w = new Whatev();
var v = w;  // Both `w` and `v` refer to the same instance of Whatev.
w.mutateIt();
// The object to which `v` refers is mutated too, of course.

Arrays

var a = [5, 66, 7];
a[1];      //=> 66
a[1] = 6;  // [5, 6, 7]
a.push(8); // [5, 6, 7, 8]

var x = a.pop(); //=> 8, and it's removed from `a`

a.unshift(4); // prepends the 4: [4, 5, 6, 7, 8]
a.shift();    //=> 4, and leaves `a` as `[5, 6, 7, 8]`.

a.insert(idx, 99); // Inserts 99 into `a` at `a[idx]`.

// Removes `len` elements from an array, starting at position `pos`.
var x = a.splice(pos, len);

a.length;      // the length of the array
a[a.length-1]; // last elem of the array

// Print an array:
trace(a);
// Another way:
Sys.println(a);
// Another way, to see more how the sausage is made:
trace(haxe.Json.stringify(a));

// Array comprehension (creates an array)
var a = [for (i in 5...10) i];

// sort an array, in-place
a.sort(Reflect.compare);
// or, if you want to write your own comparison function:
a.sort((a, b) -> a - b) // works for sorting numbers only
// or, more general:
a.sort(
    (a, b) -> {
        if (a < b) return -1;
        else if (a > b) return 1;
        else return 0;
    }
);

// Note, Haxe doesn't do array index bounds checking:
a = [5, 6, 7];
a[3]; //=> 0 (same result for an array of floats)

a = [true, true, true];
a[3]; //=> false

a = ["i", "j", "k"];
a[3]; //=> null

See the arrays article of the code cookbook for more examples.

See also the Array API docs.

Maps

var m = ["a" => 1, "b" => 2];
// Note though how it's formatted when printed out:
trace(m); // {b => 2, a => 1}

m["a"];     //=> 1
m["c"] = 3; // sets a key/value pair.

// Note, not bounds-checked:
m["d"]; //=> null

// Map comprehension.
var m = [for (i in 5...8) i => '*${i}*'];

See the maps article in the code cookbook for more examples.

See also the Map API docs.

Sets

Haxe doesn’t come with a Set type out of the box, but you can use the thx.Set type from the thx.core library, as described later in the section about using libraries from the Haxe library repo.

Usage looks like:

var s = Set.createInt(); // a set of Ints
s.add(5); // returns `true` if `s` is changed
s.add(12);
s.add(5);  // no effect (returns `false`)
s.add(7);
s.length;  // 3

See the thx.Set API docs.

Type Parameters

If you want to create a new empty Array or Map, you can use the new keyword and also specify what type the Array or Map will contain. The syntax for that is:

// A new empty array of Strings.
var a = new Array<String>(); // though, you can also do:
var a : Array<String> = [];  // and get the same thing.

a.push("aa");
a.push("bb");

// A new Map where the keys are Strings and the values are Ints.
var m = new Map<String, Int>(); // though, you can also do:
var m : Map<String, Int> = [];  // and get the same thing.
m["a"] = 1;
m["b"] = 2;

The types written in the angle brackets are the type parameters.

Enums

Built-in support for enums:

enum Size {
    Small;
    Medium;
    Large;
}
var s = Size.Small;  // Ok
var s = Small;       // Ok too
var s = Size.XLarge; // Error
var s = XLarge;      // Error

But they can do more than that. See:

Anonymous Structures

If you just need a simple structure to group heterogeneous data (recall, Maps’ values must be all of the same type), and you don’t need a class, you might use an anonymous structure:

var p1 = {n: 10, name: "Ron"};
var p2 = {n: 12, name: "Tammy 1"};
var p3 = {n: 15, name: "Tammy 2"};

p1.n;    //=> 10
p2.name; //=> Tammy 1

They can contain and be nested in other data structures, as you’d expect.

Don’t confuse anonymous structures with maps even though they print out similarly in the terminal:

var m = ["a" => 1, "b" => 2];
// prints as {b => 2, a => 1}        <-- curlies!
// type is haxe.ds.Map<String, Int>

var x = {a: 1, b: 2}; // This is an anon struct, not a map!
// prints as {a : 1, b : 2}
// type is {b : Int, a : Int}

(Well, and if you’re familiar with Python: Haxe anon structs look very similar to Python maps.)

To avoid repetition you can use typedef:

typedef SomeType = {n: Int, name: String};
// or
typedef SomeType {
    var n    : Int;
    var name : String;
}

For more info, see the anon structs manual chapter.

Operators

Haxe has all the usual operators you’d expect.

It also supports both pre- and post- increment/decrement, and they work like you’d expect.

Haxe supports the ternary operator (foo ? bar : baz), but you can often nearly as easily use if/else as an expression instead:

var x = someCond ? "this" : "that";
// or
var x = if (someCond) "this" else "that";

Division of numbers always gets you a Float.

Logical “and”, “or”, and “not” are: &&, ||, !. These and also the comparison operators (<, >, etc.) result in a Bool.

&& and || short-circuit, like you’d expect.

All the usual compound assignment operators (like +=, *=, etc.) are supported.

Haxe also supports the range operator, 5 ... 8, which gets you 5, 6, 7.

There’s no operator for exponents; you use Math.pow().

Control Flow

Haxe supports the usual control flow statements, and many are actually expressions in Haxe:

for (e in someArray) { /*...*/ }

for (i in 0 ... 5) { /* 0 to 4 */ } // or
for (_ in 0 ... 5) { /* if you don't care about that idx value */ }

// Can use `break` and `continue` while looping.

for (k => v in someMap)   { /*...*/ }
for (v in someMap)        { /* just the values */ }
for (k in someMap.keys()) { /* just the keys */ }

while (someCond) { /*...*/ }

do {
    //...
} while (someCond);

Control flow statements that expect a Bool must get a Bool; there’s no notion of “truthiness” and “falsiness” like in scripting languages.

if (someCond) someExpr;

if (someCond) { /*...*/ }
else if (otherCond) { /*...*/ }
else if (yetAnother) { /*...*/ }
else { /*...*/ }

Note, since blocks evaluate to an expression, if you only have one expression in the if body anyway, you can do:

if (someCond) someThing
else if (otherCond) somethingElse
else if (yetAnother) somethingOther
else thatsIt;

Switch:

switch x {
    case 1: trace("x was 1!");
    case 2: trace("x was 2!");
    default: trace("Hm...");
}

There’s much more to the switch statement though, since it supports pattern matching.

Scoping

Scoping is lexical, and works just as you’d expect it ought to.

You can create a block with its own local scope, and it works like you’d expect:

var x = 3;

{
    var x = 18;  // new local, shadows outer `x`
    var y = 5;   // new local

    trace(x);    // 18
    trace(y);    //  5
}

trace(x);  // 3
trace(y);  // ERROR, `y` out of scope here

The value of the block is the value of its last expression.

Loop variables are scoped to inside the loop body:

for (i in 5 ... 8) {
    trace(i);
}
trace(i);  // ERROR, `i` is out of scope here

Regular Expressions

Haxe has built-in support for and literal syntax for regexes (ex. var rx = ~/[a-f]+/;). For more info, see:

Exceptions

Haxe has try/catch and throw for exception handling. See the try/catch manual chapter for more info.

Libraries, Packages, and Modules

Haxe packages correspond to directories, and Haxe modules correspond to .hx files. Package names (and their corresponding directory names) are lowercase and may contain underscores. Module names (and their corresponding filenames) are capitalized and CamelCase. Modules contain types, and type names are also written CamelCase.

Take for example the file foo/bar/Baz.hx containing class Baz. The module Baz resides in package foo.bar (and at the top of Baz.hx it says package foo.bar;). The fully-qualified module name is foo.bar.Baz. The fully-qualified typename for class Baz is foo.bar.Baz.Baz. In general, you access the types in a module like so:

{pkg-name}.{module-name}.{type-name}

However, for that special case where module name == type name, Haxe allows you the shortcut of writing foo.bar.Baz to mean foo.bar.Baz.Baz.

Haxe finds modules by searching its classpath (see the --class-path|-p compiler option), and by looking where --library|-L specifies.

Name Visibility

Importing a module allows you to use its unqualified module name (without its package name) in your code. You could go ahead and use modules without importing them, but then you’d have to fully-qualify them with their package name.

import foo.bar.Baz;

// then access `foo.bar.Baz` as just `Baz`.

If your module contains more than one type, any types named differently than the module name are called “sub-types”. If you import a module containing subtypes, they’ll be available unqualified just like the type named after the module. That is to say, when you import a module, you get all of its types.

Notice that, if foo/bar/Baz.hx contains a Moo public subtype, you can refer to that in your code as foo.bar.Moo, rather than foo.bar.Baz.Moo.

Libraries

Libraries are archive files you download from the online Haxelib library repository using the haxelib tool. They contain modules residing within packages.

Alas, because many types of archive files are sometimes referred to as “packages”, the term “package” is sometimes casually used as a synonym for “library”.

Using the Standard Library

Using the standard library (documented at https://api.haxe.org/) doesn’t require any special imports or compiler arguments.

See the Haxe code cookbook for examples using the standard libary, though here’s a couple of choice tidbits anyway.

Parsing Strings, Math, and Random

var s = Std.string(whatev)      // Whatev to String

var n = Std.int(5.8);           // Float to Int, but truncates (so, 5)
var n = Math.round(5.8);        // Float to Int                (so, 6)
var x = Math.fround(5.8);       // Float to Float              (so, 6.0)

var n = Std.parseInt("5");      // String to Int
var n = Std.parseInt("05");     // Same. Leading zeros are ignored.
var n = Std.parseInt("5.7");    // String to Int, and truncates
var x = Std.parseFloat("5.2");  // String to Float

// Note that `Std.parseInt("")`   ==> null
// and       `Std.parseFloat("")` ==> NaN

var r1 = Math.random();  // Float, 0 to < 1
var r2 = Std.random(n);  // Int,   0 to < n

stdin, stdout, stderr

Reading from stdin and writing to stdout and stderr.

stdin

You can read from stdin interactively from the command line, or can pipe input to your Haxe program like you would with any other command line utility.

To read in one line:

Sys.println("Enter your name:");
var ans = Sys.stdin().readLine();
// `ans` is just the text --- no newline

If you want to iteratively read in lines:

var line : String;
var lines : Array<String> = [];
try {
    while (true) {
        line = Sys.stdin().readLine();
        lines.push(line);
    }
}
catch (e : haxe.io.Eof) {
    trace("done!");
}

You could also read in all the input in one shot:

var content = Sys.stdin().readAll().toString();

stdout

There’s a few ways to write to stdout:

trace("Hello, trace!");
Sys.println("Hello, println!");
Sys.print("Hello, print!"); // no added newline

You can also use Sys.stdout() to grab the stdout object and call its write methods (see haxe.io.Output).

stderr

To write to stderr:

Sys.stderr().writeString("Yow!\n");

Command Line Args

Sys.args(); //=> array of args passed (Strings)

Static Extension

Haxe can do a special trick, strictly for convenience: If there’s a Type (typically not one you’ve written yourself; call it, say, SomeClass) to which you’d like to add member functions, you can:

  1. Create a SomeStaticExtClass containing static methods which you wish SomeClass had as member functions. Those static methods should take an instance of SomeClass as their first argument.
  2. Back in your Main.hx, instead of import some_pkg.SomeStaticExtClass, do using some_pkg.SomeStaticExtClass. This makes the magic happen.
  3. Now when you make these wished-for method calls on instances of SomeClass, Haxe will behind-the-scenes translate them into calls to those static methods with the instance passed in as the first argument.

A few modules in the standard library were written with this usage in mind. See the static extension chapter of the manual.

Using Libraries from Haxelib

The online repository of Haxe libraries and also the library management command line tool are both called “haxelib”. When you install the binary release of Haxe onto GNU/Linux, it comes with both haxe (the compiler) and haxelib (the library management tool). See also haxelib --help.

Libraries you install via haxelib are often (though not always) named lowercase with dashes where necessary. They are sometimes named using dots instead of dashes.

Note that the name of a library does not necessarily have anything to do with the packages and modules it contains. For example, the “thx.core” library contains modules in the thx package (there’s no “thx.core” package).

Unfortunately, if you name libraries with dots they tend to look like package names, which can be confusing. I think dashes or camelCase may make more sense.

A library archive file itself is a .zip file, and its haxelib.json file is the library’s config file.

By default, haxelib installs and unpacks libraries into your ~/haxelib directory. This is your local haxelib-installed libraries repo. Haxelib can also manage project-specific repos.

To use a library from the haxelib library repository:

  1. install the library you want, ex.: haxelib install cool-stuff
  2. Into your hl.hxml build file add a line like --library cool-stuff
  3. In your code, if you’d prefer not to use fully-qualified typenames, import modules from the library.

For more info, see the haxelib docs.

Example: CSV files

To work with csv files, try the thx.csv module. Install the lib:

haxelib install thx.csv

and add the line --library thx.csv to your hl.hxml file. In your code, import thx.csv.Csv and use this module like so:

// Read in the csv content as a string.
var s = "Size,Color,Shape
3,green,round
5,red,square";

// Gets you an array of array of strings.
var aoa = Csv.decode(s);

trace(aoa);

Example 2: Set Type

Install haxelib install thx.core, and import thx.Set. See Sets above for usage.

--class-path vs --library

Note the difference between the two following haxe compiler options:

--class-path|-p
This specifies any additional directories where the compiler is to look for classes (the classpath).
--library|-L
This specifies a particular library (typically in your ~/haxelib) that you want included into your build.

Note, you don’t add the path to the Haxe standard library to your classpath — that’s what the $HAXE_STD_PATH environment variable is for. For example, with my installation the Haxe standard library is located in ~/opt/haxe/std, and so in my ~/.bashrc I have:

export HAXE_STD_PATH="$HOME/opt/haxe/std"

Documenting Your Library

To generate HTML API docs, use dox.

It works like this:

  1. document your libraries with doc comments like /** ... */.

  2. to be continued

Recommended Libraries

Console

For coloring output in the terminal, try Console.hx. Super easy to use.

Utility Libraries

todo

Unit Testing

One popular testing library is munit.

See also discussion on other options.

Links

HaxeUI (GUI toolkit):

More links:

What’s Next?

Start a fun project, and go read the Haxe Manual to learn about all the more advanced stuff not covered in this tutorial! Enjoy, and see you at the forum!

TODO


  1. See the full list of Haxe compiler targets.

  2. Via externs, which are currently out of the scope of this tutorial.

  3. See Editors and IDEs.