2020-06
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, statically-typed, 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 using its own built-in interpreter.
For a short sample of some Haxe code, visit haxe.org 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} works 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 would do. :)
This document covers Haxe version 4. If you don’t already have it installed, and you’re on GNU/Linux, you might have a look at my short getting-started guide.
Some characteristics of Haxe:
if
/else
, switch
/case
, etc. — more about this below)See also the official Language Introduction.
haxelib
tool.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:
- The above list of languages are all open source, while the Haxe compiler is free software. Read more about the differences between open source and free software.
- The evolution of each of the above languages is determined by its owner’s corporate interests. I’m delighted to note that Haxe is free from such influence.
This tutorial was written using Haxe 4 on GNU/Linux, utilizing haxe
’s built-in interpreter (“interp”, aka “eval”). 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”.
Create and populate your project directory:
with src/Main.hx containing:
and interp.hxml containing:
-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) — that is, when compiling to some target platform — you’d usually have to type two separate commands:
- a
haxe
invocation to compile/build your program, and then- a target-specific command to run or otherwise use the output that
haxe
just produced.Using interp (aka eval), it’s just the one step though.
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 a target-specific some-target.hxml file containing those options,
-p src
-m Main
--some-target whatev
and then type haxe some-target.hxml
to get the same result.
If you build your project for multiple different targets, you’ll likely 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 #
.
// single-line comment
/*
A multi-line
comment.
*/
/* Some people like to write
* them with asterisks on the
* left-hand-side.
*/
/**
A multi-line _doc_ comment.
Indent however you like.
*/
Haxe is not whitespace-sensitive.
Haxe is case-sensitive. Variable names are often written camelCase, with types capitalized LikeThis.
null
is used to indicate when a variable refers to no object.
Void
is used to indicate the absence of type (ex. when used as the return type of a function).
The so-called “basic types” are Bool, Int, and Float, with literals like:
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 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 (no interpolation)
'allows ${foo} interpolation' // single-quoted
"concat" + "enate strings"
"both single- and double-quoted strings
can be written across multiple lines and
both can contain escapes like \t and \n."
See the String API docs for full list of String fields (ex. length
, charAt()
, charCodeAt()
, indexOf()
, split()
, …).
You use var
or final
to define variables. Variables are typed. Thanks to type inferencing, you don’t often need to explicitly specify the type of a variable:
final
variables can’t be reassigned to another object, but they can refer to a mutable object (which could itself be modified).
On HashLink (and other statically-typed targets):
When types cannot be inferred you must include them explicitly. Type names come after the variable name and colon, for example:
// (The type can be inferred here, but to show the syntax...)
var x:Int = 5;
// You could put in extra spaces if you want, but that's not customary.
var x : Int = 5;
To see the type of any variable, use $type(x);
which will print out a warning line to the console at compile-time indicating x
’s type.
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, aka instance) or static. Summarizing:
instance | static | |
---|---|---|
variable: | member variable | static variable |
function: | (member) method | static method |
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.
By default, all fields are private and member.
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();
}
}
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 basic types.
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:
You return values with a return
statement.
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.
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]`.
a.remove(99); // Removes first occurrence of 99.
// Removes `len` elements from an array, starting at position `pos`.
var x = a.splice(pos, len);
// So,
// * add stuff with: push, unshift, and insert:
// "push/unshift onto", "insert into"
// * remove stuff with: pop, shift, splice, and remove:
// "pop/shift off", "splice out of", "remove from"
a.length; // the length of the array
a[0]; // first elem 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 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 generally:
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.
var m = ["a" => 1, "b" => 2];
// Note though how it's formatted with curlies when printed out (as of Haxe 4.0.5):
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}*'];
// If you really need to know the length of one...
Lambda.count(m);
See the maps article in the code cookbook for more examples.
See also the Map API docs.
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.
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.
Built-in support for enums:
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:
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/dicts.)
Note that usually in Haxe when you see a
:
there’s a type after it — not so for anon structs though. My advice: when writing types after a variable, omit any extra spaces. When writing anon structs, put that space after the colon. I believe the Haxe code formatter does this as well.
To avoid repetition you can use typedef
:
Note, that typedef cannot be declared inside a class.
For more info, see the anon structs manual chapter.
Haxe has 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:
Division of numbers always gets you a Float.
Logical “and”, “or”, and “not” are spelled &&
, ||
, !
. 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; explicitly use Math.pow()
.
Haxe supports the usual flow control statements, most (all?) of which are actually expressions in Haxe:
// Loop over an array.
for (e in someArray) { /*...*/ }
for (i in 0...someArray.length) { /*... someArray[i] ...*/}
for (i in 0...5) { /* 0 to 4 */ } // or
for (_ in 0...5) { /* if you don't care about that idx value */ }
// You can use `break` and `continue` while looping.
// Loop over a map.
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);
Flow control statements that expect a Bool must get a Bool (for example, as returned by the equality and comparison operators); 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:
There’s much more to the switch statement though, since it supports pattern matching.
Scoping is lexical, and works just as you’d expect it ought to.
Further, 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:
Haxe has built-in support for and literal syntax for regexes (ex. var rx = ~/[a-f]+/;
). For more info, see:
Haxe has try/catch and throw for exception handling. See the try/catch manual chapter for more info.
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 also by looking where --library|-L
specifies.
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 every time you use them.
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.
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 (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.
var s = Std.string(whatev); // Whatev to String, or
var s = '${whatev}'; // this. There's also a
var s = '$whatev'; // shorthand for simple case.
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 x = n * 1.0; // Int to Float :)
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
Reading from stdin and writing to stdout and stderr.
You can read from stdin interactively from the command line. You can also pipe input to your Haxe program just as 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(); // a String, no newline
lines.push(line);
}
}
catch (e:haxe.io.Eof) {
trace("done!");
}
You could also read in all the input in one shot:
There’s a few ways to write to stdout:
trace("Hello, trace!"); // Provides filename, line number, and a newline.
Sys.println("Hello, println!"); // Provides a newline.
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).
To write to stderr:
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:
import some_pkg.SomeStaticExtClass
, do using some_pkg.SomeStaticExtClass
. This makes the magic happen.A few modules in the standard library were written with this usage in mind. See the static extension chapter of the manual.
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 using dashes makes 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:
haxelib install cool-stuff
--library cool-stuff
Examples:
It’s often very useful to install packages directly from their git repo, rather than waiting for their lib.haxelib.org package to be updated. To do that, for example, for the thx.core library:
haxelib git thx.core https://github.com/fponticelli/thx.core.git
(see docs for haxelib git)
If you’re working on a library on your own system, which you’d like to install (locally, into your ~/haxelib) straight from its project directory and without having to first publish it to lib.haxe.org, you can do:
haxelib dev my-proj ~/path/to/my-proj
For more info, see the haxelib docs.
To work with csv files, try the thx.csv module. Install the lib:
To your .hxml file, add the line --library thx.csv
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);
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
--library|-L
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:
To generate HTML API docs, use dox.
It works like this:
document your libraries with doc comments like /** ... */
.
to be continued …
For coloring output in the terminal, try Console.hx. Super easy to use.
todo
One popular testing library is munit.
See also discussion on other options.
Docs:
Misc:
HaxeUI (GUI toolkit):
More links:
@foo
and @:bar
)See the full list of Haxe compiler targets.↩
Via externs, which are currently out of the scope of this tutorial.↩
See Editors and IDEs.↩