In this section, we will look at
- VSCode project setup
- basic building blocks
Currently, one the best IDEs with Haxe support is Visual Studio Code. Once the base VSCode is installed, install the Haxe Extension Pack.
Let's keep using our hello world project. Open VSCode, then open the hello world project folder by "File" -> "Open...".
The Haxe language support extension automatic detects the present of .hxml
file. It allows us to use the hotkey ctrl
+shift
+b
(or command
+shift
+b
for Mac) to build the project.
Let's take a closer look at our HelloWorld.hx
source code:
class HelloWorld {
static function main() {
trace("Hello, World!");
}
}
In Haxe, each .hx
source file is a module, in which there can be zero, one, or more types. class
is one of the most common kinds of type being used. There are also interface
, typedef
, enum
, and abstract
, which we will cover some of them later.
In our HelloWorld
module, we defined a HelloWorld
class, and tell the Haxe compiler that this is the main entry point of the program by -main HelloWorld
. A main class should have a special main()
static function, to be called when the program starts.
In our main()
, we used trace()
, which is a special function for debugging. By default, trace()
will print out its argument to the console.
Use the var
keyword to declare a variable.
class HelloWorld {
static function main() {
var str = "Hello, World!";
trace(str); // Hello, World!
}
}
Each variable has a type. A variable of String
(a sequence of characters) cannot store a Float
(a floating point number). There will be compilation error if we try to do so:
class HelloWorld {
static function main() {
var str = "Hello, World!";
str = 123.45; // Float should be String
trace(str);
}
}
It helps us by preventing us to use invalid operation to a value of incompatible type. For example, we can multiply a Float
number, but not multiply a String
.
class HelloWorld {
static function main() {
var f = 123.45;
trace(f * 2); // 246.9
var str = "Hello, World!";
trace(str * 2); // compilation error
}
}
We can force the compiler to ignore the type error and output the expression as is, but then the program's behavior will be undefined (mostly depended on the target).
class HelloWorld {
static function main() {
var str = "Hello, World!";
trace(untyped str * 2);
/*
interp: error
js: NaN
python: Hello, World!Hello, World!
*/
}
}
Notice how some of the targets produce somewhat reasonable outputs, but different from each other. It is exactly why cross-platform development can be painful - we have to remember in what cases, the behavior will be different across platforms. Haxe helps by defining a single reasonable behavior across all its targets, and prevent us from doing tricky things that may lead to inconsistent behavior across targets.
Let's say we are in fact interested in Python's behavior - to repeat the String
by n times. To do this in a cross-platform way, we have to define our own function.
class HelloWorld {
static function repeat(str:String, n:Int):String {
var result = "";
for (i in 0...n)
result += str; // same as `result = result + str;`
return result;
}
static function main() {
var str = "Hello, World!";
trace(repeat(str, 2)); // Hello, World!Hello, World!
}
}
The above repeat()
implementation is not the best in anyway, but we will use it for learning a few things.
Notice how I declare the function arguments inside the brackets, with their type information (:String
and :Int
). The type annotations are optional, but for documentation purpose, we usually add them anyway. Here we used another build-in type, Int
, which is integer.
After the close bracket, there is another type annotation (:String
), which defines the return type of the repeat()
function. It is also optional, but good to include anyway. Notice we didn't add one to main()
. To define a return type for main()
, we may use Void
, which means nothing, because we do not return
anything from main()
;
We used a loop expression in repeat()
. Specifically, it is a for-loop. Its syntax is for (variable in start...end) ...
. It introduces a variable, and the variable's value will go from start
to end
(excluding end
). In the case of our repeat(str, 2)
, the i
in the for-loop will be 0
and then 1
, resulting in running the content of the loop twice.
When we want to repeat more than one expression in the loop body, we can use a block expression, which is exactly a group of expressions. For example, if we want to print out the loop variable in the for-loop:
static function repeat(str:String, n:Int):String {
var result = "";
for (i in 0...n) {
trace("i is " + i);
result += str;
}
return result;
}
Did you notice that we have been using block expressions as function bodies? So, right, a function with a single expression doesn't need a wrapping {}
:
function hello(name) trace("Hello, " + name + "!");
Sometimes we want to loop in a different way (for example we may want i
's value to go from n
to 0
instead of 0
to n
), we have to use a while-loop (or a do-while-loop), which is more flexible. Rewriting our repeat()
with a while-loop, we will have:
static function repeat(str:String, n:Int):String {
var result = "";
var i = 0; // loop variable
while (i < n) { // loop condition
result += str;
i = i + 1; // modify the loop variable in each iteration
}
return result;
}
There is a third way to perform repeating tasks - recursion, which is calling the same function within the function body. Rewriting our repeat()
with recursion, we will have:
static function repeat(str:String, n:Int):String {
if (n < 0)
throw "n cannot be less than 0";
else if (n == 0)
return "";
else
return str + repeat(str, n - 1); // call `repeat()` again
}
Recursion solves problems by using the divide and conquer technique. It is particularly important and popular in the functional programming paradigm. If you are not used to this way of thinking, spend some time to understand why the recursion version of repeat()
works.
You should have noticed that we also added input validation in the recursion version. When n
is negative, the repeat operation doesn't make sense, so we just throw
an error. We should also do that even when using for or while loop:
static function repeat(str:String, n:Int):String {
if (n < 0)
throw "n cannot be less than 0";
var result = "";
for (i in 0...n)
result += str;
return result;
}
Let's see what happen when there is an error. Modify main()
to call repeat(str, -1)
, compile and run, we will get:
HelloWorld.hx:4: characters 12-17 : n cannot be less than 0
HelloWorld.hx:13: characters 14-29 : Called from
?:1: characters 1048575-8796094070782 : Called from
Aborted
The terminal process terminated with exit code: 1
The program terminated immediately when the error is thrown. Sometimes we want to handle the error and let the program continue. To do so, we can use try ... catch(v) ...
:
class HelloWorld {
static function repeat(str:String, n:Int):String {
if (n < 0)
throw "n cannot be less than 0";
var result = "";
for (i in 0...n)
result += str;
return result;
}
static function main() {
var str = "Hello, World!";
try {
trace(repeat(str, -1));
} catch(e:String) {
trace("There is an error... the error is: " + e);
trace("Let's repeat 1 time instead: ");
trace(repeat(str, 1));
}
}
}
Right now, the repeat()
function is completed with error handling. To make it reusable in another project, let's move it to its own module.
Create a folder, named utils
. Within the utils
folder, create RepeatString.hx
file, with the content as follows:
package utils;
class RepeatString {
static public function repeat(str:String, n:Int):String { // notice `public`
if (n < 0)
throw "n cannot be less than 0";
var result = "";
for (i in 0...n)
result += str;
return result;
}
}
Modify HelloWorld.hx
to:
class HelloWorld {
static function main() {
var str = "Hello, World!";
trace(utils.RepeatString.repeat(str, 2)); // Generally, use the `.` notation to access members of something.
}
}
Referencing the RepeatString
class with utils.RepeatString
is tedious. We can add an import
statement at the top such that we can use RepeatString
directly:
import utils.RepeatString;
class HelloWorld {
static function main() {
var str = "Hello, World!";
trace(RepeatString.repeat(str, 2));
}
}
To further simplify things, we can import the repeat()
function directly:
import utils.RepeatString.repeat;
class HelloWorld {
static function main() {
var str = "Hello, World!";
trace(repeat(str, 2));
}
}
Moreover, wildcards (*
) can be used to import every type or every public static member of a type:
import utils.*; // import all the types in the utils package
import utils.RepeatString.*; // import all the public static member of `RepeatString`
The repeat()
looks really nice now, and it works in every Haxe target, including Python. But think about it, we already knew that we can use str * number
to repeat a String
, can we just use that for the Python target, and use our implementation for all other targets?
Why not.
package utils;
class RepeatString {
static public function repeat(str:String, n:Int):String {
if (n < 0)
throw "n cannot be less than 0";
#if python
return untyped str * n;
#else
var result = "";
for (i in 0...n)
result += str;
return result;
#end
}
}
-
Try to compare the Python output before and after using conditional compilation.
-
Add the
inline
keyword in front ofstatic public function repeat(...
, see how it affects the Python output and the JS output.
We have used String
, a sequence of characters. What if we want to store a sequence of value of arbitrary type? Use Array
.
class HelloWorld {
static function main() {
// to define an Array, use the square brackets
var numbers = [1, 2, 3, 4, 5];
// also use square brackets for accessing the elements
// notice that the array "index" starts from 0
var thirdElement = numbers[2];
trace(thirdElement); // 3
// an Array is "iterable"
for (element in numbers) {
trace(element); //1, 2, 3, 4, 5
}
// an Array is "mutable"
numbers.push(6); // add 6 to the end
trace(numbers); // [1, 2, 3, 4, 5, 6]
var lastElement = numbers.pop(); // remove the last element
trace(lastElement); // 6
trace(numbers); // [1, 2, 3, 4, 5]
// to create an empty Array, use [] or new
var emptyArray = [];
var alsoEmptyArray = new Array();
}
}
Can you complete the following RepeatValue.repeat
function, such that utils.RepeatValue.repeatInt(1, 3)
returns [1, 1, 1]
, and utils.RepeatValue.repeat(3.14, 3)
returns [3.14, 3.14, 3.14]
?
package utils;
class RepeatValue {
static public function repeatInt(value:Int, n:Int):Array<Int> {
throw "implement this";
}
// `repeatInt` can only repeat `Int`, the following can repeat any type!
// Notice the `<T>` "type parameter"!
static public function repeat<T>(value:T, n:Int):Array<T> {
throw "implement this";
}
}
Previously, we have been defining static
functions, which are members of the class itself. Notice we have used Array
's .push()
and .pop()
methods, and those are the members of an Array
"instance".
Classes are good for modeling objects that have a mutable state, as well as a set of methods that depends on its state.
Let's try define our own classes, Person
and SecretAgent
:
class Person {
private var name:String;
public function new(name:String) {
this.name = name;
}
public function getName():String {
return name;
}
}
class SecretAgent extends Person {
override public function getName():String {
return "Smith";
}
}
class HelloWorld {
static function askName(p:Person):Void {
trace(p.getName());
}
static function main() {
var andy = new Person("Andy");
askName(andy); // Andy
var kevin = new SecretAgent("Kevin");
askName(kevin); // Smith
}
}
Notice how SecretAgent
inherits the constructor of Person
and overrides the getName()
method.
We don't always have to create a class to store a bunch of data. Most of the time, we can simply use an anonymous object.
class HelloWorld {
static function main() {
var andy = {
name: "Andy",
occupation: "researcher"
};
var kevin = {
name: "Kevin",
occupation: "secret agent"
};
trace(andy.name); // Andy
trace(kevin.name); // Kevin
}
}
Notice that there is no concept of private, so kevin
cannot really protect it's real name
by itself.
An anonymous object is anonymous because it is an object without a class. Although without a class, there is still a structural type associated with the object.
For the above example, the type of andy
and kevin
can be declared as follows:
// give the structural type an "alise" for easy reference
typedef Person = {
name:String,
occupation:String
}
class HelloWorld {
static function main() {
var andy:Person = {
name: "Andy",
occupation: "researcher"
};
var kevin:Person = {
name: "Kevin",
occupation: "secret agent"
};
trace(andy.name); // Andy
trace(kevin.name); // Kevin
trace(andy.height); // error: Person has no field height
}
}