Haxe 4.3.0 is here! Here's what you need to know.
development
haxe
]
Haxe finally released the long-awaited 4.3.0 update today, bringing with it not just many new bug fixes, but also brand new language features you can use in your own projects. Haxe 4.3.0 sees a lot of improvements for those using abstracts, type parameters, multi-threading, and macros, as well as some new syntax which will be incredibly useful in any project.
I tried to include more detail than the original release notes, to make sure people understand each feature without having to dive into individual pull requests.
Haxe 4.3.0 is currently available on Haxe.org.
New Language Features (All Targets)
Here, I’ve taken each of the new language features and provided a more in-depth explanation of how they work, along with a demo in try.haxe.org (which has 4.3.0 now!). Please check out the demos to get a better idea of what I’m talking about with each entry!
support defaults for type parameters
- Try Haxe: Demo
- Haxe Evolution: Read the approved proposal
- This language feature is related to: Haxe Manual: Type parameters
- Alongside using
<T:Foo>
to constrain a type parameter to only allow types extending Foo, you can now specify a default type parameter, used when the type parameter is not specified. - If
Foo
is declared asclass Foo<T=String>
, thenvar test:Foo;
will be equivalent tovar test:Foo<String>;
, but notvar test:Foo<Int>
.- Without a default type parameter,
var test:Foo;
will throw a compilation error.
- Without a default type parameter,
- Default types can be used alongside constraints like so:
class Bar<T:String=String>
- Specifying the default parameter first and attempting to constrain it second will not work.
support @:op(a()) on abstracts
- Try Haxe: Demo
- Haxe: Read the original issue
- This language feature is related to: Haxe Manual: Operator overloading for abstracts
- In the same manner that abstracts could override operations like
+
and-
before, you can now override the function call operator()
to essentially make an abstract that acts like a callable function. - Since this operation override resolves based on the parameters provided to it, you can have multiple functions annotated with
@:op(a())
and the correct one will be chosen. Note the functions still have to be named different on the abstract itself. - This acts a lot like function overloading, a language feature used commonly in Java and C#.
- Overloading is a feature highly anticipated for Haxe, but the implementation is limited to externs and inline functions for now.
- I have hopes that overloading will become a standard feature in the relatively near future (maybe 4.4?) but implementing it properly is really tough and the proposal is still pending: HaxeFoundation/haxe-evolution#93
support abstract keyword to reference the abstract
- Try Haxe: Demo
- Haxe Evolution: Read the approved proposal
- This language feature is related to: Haxe Manual: Abstracts
- Normally, within an abstract, the
this
keyword points to the value of the underlying type. This is what lets you initialize abstracts withthis = value
in the constructor. - However, this leads to the problem of referring to methods of the abstract itself, or attempting to pass the abstract itself as a value. Before 4.3.0, you would have to use
(cast this:MyAbstract)
to do this. - Now, the
abstract
keyword pulls double-duty; aside from being used to declare a class, it can also be used as an identifier within abstracts to refer to the abstract itself. - For example, for an abstract adding the function
foo()
,this.foo()
would fail, butabstract.foo()
will call the correct function.
support static var at expression-level
- Try Haxe: Demo
- Haxe Evolution: Read the approved proposal
- This language feature is related to: Haxe Manual: Static access modifier
- You can now initialize static variables within a function. This essentially acts like a static variable of the class, but bound to the scope of the function.
- For example, you can put
static var x = 0
at the beginning of a function, and this creates a static variable namedx
which is only accessible from within that function.
Some great example use cases for this include:
- Caching values. You can do something like this while having the assurance that no other functions in your class will accidentally modify
foo
and break it.public static function getFoo():Void { static var x:Foo = null; if (x == null) x = new Foo(); return x; }
- If you have a native function reference, you can load it once, lazily, while making it inaccessible without calling the wrapper function.
- If you have a function which recursively calls itself, you can define a variable to keep track of the depth and return a fallback if you are too deep.
support ?. safe navigation operator
- Try Haxe: Demo
- Haxe Evolution: Read the approved proposal
- This feature adds a brand new operator with new syntax to the language.
- Typically, calling
foo.bar
whenfoo
is null throws an error.- To resolve this, you would have to either check that
foo
is null with a conditional statement, enable Null Safety, or use a try/catch block. - This problem gets exceptionally more annoying if you are trying to access
foo.bar.baz.bat.ball.bin
. Checking nullability with conditions is a pain in the ass, rewriting your application to enable null safety may be difficult or even impossible (what iffoo
is the output of a library you don’t control the code for?), and a try/catch block is still pretty inconvenient.
- To resolve this, you would have to either check that
- You can now use
foo?.bar
, which will returnnull
iffoo
happens to be null.- You can now do
var result = foo?.bar?.baz?.bat?.ball?.bin;
to skip past all those null checks without crashing. If any offoo
,bar
,baz
,bat
,ball
, orbin
is null,result
will be null.
- You can now do
- This is especially powerful in combination with the new null coalescing operator, covered next.
added ?? null coalescing operator
- Try Haxe: Demo
- Haxe Evolution: Read the approved proposal
- This feature adds a brand new operator with new syntax to the language.
- This condenses a null check conditional statement or ternary into a single operator.
- The expression
value ?? "NULL VALUE"
resolves to the string"NULL VALUE"
ifvalue
is null, and whatever the value ofvalue
is otherwise. - This complements the safe navigation operator incredibly well.
add -w compiler option to configure warnings
- This is not available on Try Haxe due to it being a compiler option.
- Haxe: Read the merged pull request
- Added the new metadata
@:haxe.warning
, which takes a String. You can use this to enable or disable compiler warnings. - For example,
@:haxe.warning('-WVarInit')
before a function will suppress allWVarInit
warnings within that function.
added new error reporting modes
- This is not available on Try Haxe due to it being a feature for compilers and IDEs.
- Haxe: Read the merged pull request
- This adds a new compile define, which can change how compile errors display to the user.
-D message-reporting=classic
is the default reporting mode, which uses the current output:-D message-reporting=pretty
utilizes new formatting for errors:- Use
-D message-reporting=pretty -D no-color
if your terminal doesn’t support ANSI escape codes for colors.[ERROR] Main.hx:8: characters 9-18 8 | C.f("hi"); | ^^^^^^^^^ | Could not find a suitable overload, reasons follow | Overload resolution failed for () -> Void 8 | C.f("hi"); | ^^^^ | Too many arguments | Overload resolution failed for (t : f.T) -> Void 8 | C.f("hi"); | ^^^^ | Constraint check failure for f.T | String should be Int | For function argument 't'
-D message-reporting=indent
provides a similar format to the classic mode, but while using the newly provided indentation. This can be useful for IDEs.$ haxe compile-fail.hxml -D message-reporting=indent Main.hx:8: characters 3-12 : Could not find a suitable overload, reasons follow Main.hx:8: characters 3-12 : Overload resolution failed for () -> Void Main.hx:8: characters 7-11 : Too many arguments Main.hx:8: characters 3-12 : Overload resolution failed for (t : f.T) -> Void Main.hx:8: characters 7-11 : Constraint check failure for f.T Main.hx:8: characters 7-11 : String should be Int Main.hx:8: characters 7-11 : For function argument 't'
-D messages-log-file=[path]
allows you to define a file to output these messages to.-D messages-log-format=[format]
allows you to choose a different log format for the log file than from the displayed output.- For example, you can display
pretty
output to the user and storeindent
output to a file for the IDE to parse.
- For example, you can display
- In the future, VSHaxe will be updated to make use of these new reporting modes.
support custom metadata and defines
- This is not available on Try Haxe due to it being a feature for compilers and IDEs.
- Haxe: Read the merged pull request
- This allows users to define custom metadata and compile defines to the compiler, for the purposes of providing documentation and completiong.
- Actually implementing these metas and defines into code is still done via macros, in the same manner as previously, but this provides better IDE support for custom defines.
- The new
haxe
argument--help-user-metas
allows you to print the documentation for all user-defined metadata in the current compilation context. - The new
haxe
argument--help-user-defines
allows you to print the documentation for all user-defined defines in the current compilation context. - The new function
haxe.macro.Compiler.registerCustomMetadata
allows you to add documentation for a single metadata. - The new function
haxe.macro.Compiler.registerCustomDefine
allows you to add documentation for a single define. - The new function
haxe.macro.Compiler.registerMetadataDescriptionFile
allows you to pass documentation for multiple metadatas via a file path. - The new function
haxe.macro.Compiler.registerDefinesDescriptionFile
allows you to pass documentation for multiple defines via a file path.
Just a reminder, ALL of the above features work in EVERY Haxe target. You can use these features when targeting C++, JavaScript, Java, Python, Lua, or whatever target language the Haxe compiler supports.
Standard Library Improvements
There have been many improvements to classes in the standard Haxe libraries:
haxe.ds.Vector
has a new functionfill(T)
which sets every element in the Vector to the provided value.haxe.ds.Vector
can now be instantiated using eithernew Vector(length)
ornew Vector(length, defaultValue)
to automatically callfill()
after instantiationn.- New class
sys.thread.Condition
for threaded targets.- A condition includes functionality which in other languages may be known as a
Lock
. It has functions to acquire, release, signal, or broadcast. - This thread syncronization tool allows only a single thread to access a block of code, preventing other threads from accessing until it is safe to do so.
- For example, you can wait until an object is written to a map on one thread before reading it on another thread.
- An analogy for this is like a bathroom key in a school.
- This is useful for creating thread-safe, multi-platform code.
- A condition includes functionality which in other languages may be known as a
- New class
sys.thread.Semaphore
for threaded targets.- A semaphore is a lock which allows a specific number of threads to enter a block before waiting, rather than just one.
- This thread syncronization tool allows only a specific number of threads to access a block of code, preventing other threads from accessing until it is safe to do so.
- For example, you can create a pool of connections of limited size, and essentially create a queue which waits for connections to be freed before
- An analogy for this is like a bouncer at a club, keeping occupancy within the limit despite multiple other guests attempting to enter via other threads.
- This is useful for creating thread-safe, multi-platform code.
- New function
sys.Http.getResponseHeaderValues
allows for retrieving header values when a response contains multiple headers of the same name. Sys.environment()
now returns a copy of the array, rather than an array which can be modified in place.- This makes the function’s behavior consistent across platforms.
Sys.putEnv(name, value)
now consistently deletes an environment variable whenvalue
isnull
.- This behavior was previously inconsistent across platforms.
Macro Improvements
There have been a lot of improvements and new functions for macros too:
- Certain functions of
haxe.macro.Context
will now throw a warning if they are used within an initialization macro when they shouldn’t be, and when they are used outside an init macro when they shouldn’t be.- For example,
haxe.macro.Context.typeof(expr)
will throw a warning if from an initialization macro.
- For example,
- The new function
haxe.macro.Context.onAfterInitMacros(Void->Void)
allows you to define a callback function, invoked after initialization macros are done and as typing begins.- This is used to delay typing-dependant code from your initialization macros.
- Fun fact if you’re reading this,
haxe.macro.Compiler.signature(v:Dynamic)
gives you an MD5 signature for a Haxe object. It’s existed for a while, but isn’t documented for whatever reason. Not related to the update just thought that was interesting. - The new function
haxe.macro.Context.getMacroStack()
returns anArray<haxe.macro.Expr.Position>
containing the full call stack for the current macro. This can be useful for the purposes of producing improved error messages. - The new function
haxe.macro.Context.initMacrosDone()
returns true if configuration macros are done and parsing/typing can start. This is used to separate configuration macros from typing macros better. haxe.macro.Context.makeExpr(value:Dynamic)
now allows taking Maps as arguments.haxe.macro.Context.getConfiguration()
gives you access to aCompilerConfiguration
, which is a standard means of accessing values such as:- The current version of the Haxe compiler.
- A list of all the arguments passed to the compiler, either via the command line or via
hxml
file. - Whether
--debug
is set. - Whether
--verbose
is set. - The current target platform.
- The compilation configuration for the target platform (see below)
- The path for the current main class.
- A list of access rules for certain packages (for example,
packageRules.get("java")
returnsForbidden
on the Python target).
- Added
PlatformConfig
, accessible viagetConfiguration()
, providing many details about the current compilation target’s configuration.staticTypeSystem
is true if Int, Float, Bool are not-nullable basic types on this target platform.sys
is true if this platform has access to thesys
package.overloadFunctions
is true if this platform supports overloaded functions natively (for example, the Java and C# targets use this to allow overloads in externs).reservedTypePaths
specifies type paths reserved by the target platform.supportsFunctionEquality
is true if the platform supports being able tofunction == function
.supportsThreads
is true if the target platform supports threadssupportsRestArgs
is true if the target platform supports...rest
in function arguments.- There are many other useful values available in this, check the docs.
- The new function
haxe.macro.TypeTools.toModuleType
lets you convert ahaxe.macro.Type
to ahaxe.macro.Type.ModuleType
- The new function
haxe.macro.TypeTools.fromModuleType
lets you convert ahaxe.macro.Type.ModuleType
to a ahaxe.macro.Type
- The new function
haxe.macro.Context.getMainExpr()
returns anExpr
which contains a call the application’sMain
function, if it exists.- This function will only return a non-null result from
haxe.macro.Context.onGenerate
orhaxe.macro.Context.onAfterGenerate
- This function will only return a non-null result from
- The new function
haxe.macro.Context.getAllModuleTypes()
returns an array ofhaxe.macro.Type.ModuleType
s to be generated in the output.- Be aware that modifying this array does nothing, and may change over time; it is only considered complete during the generation phase.
- The new function
haxe.macro.Context.withImports<X>(imports:Array<String>, usings:Array<String>, code:()->X)
.- This allows you to execute the given function, while temporarily adding the specified
import
andusing
statements to that code’s context. - These imports/usings do not affect any code run afterwards, even if
code
throws an exception. - The function returns the return value of the function. If you want to pass arguments into
code
, use.bind()
to create a new function with no parameters.
- This allows you to execute the given function, while temporarily adding the specified
- The new function
haxe.macro.Context.withOptions<X>(options, code:()->X)
- This allows you to execute the given function, while temporarily modifying some compiler options.
- The compiler is restored to default behavior afterwards, even if
code
throws an exception. - The options available include:
allowInlining
to enable or disable inlining during typing withtypeExpr
allowTransform
to disable some abstract type transformationns. The typed code will be almost exactly the same as the input code.
- The new function
haxe.macro.Context.makeMonomorph
creates a newTMono
type, which can be used withContext.unify
to make the compiler bind the monomorph to an actual type. - The new compiler option
-D eval-print-depth=###
allows you to set the maximum depth when attempting to print an eval.- This is useful if you are debugging, and want to look deep inside a block of a macro statement.
- The new compiler option
-D eval-pretty-print
causes eval values to be indented when printing.- This massively improves the experience of lookinng at deep eval blocks, especially when
eval-print-depth
is increased.
- This massively improves the experience of lookinng at deep eval blocks, especially when
Target-specific Improvements
[JVM]
The--jvm
option now supports taking a directory as an argument, rather than a JAR file path.- This will export compiled Java
.class
files into the folder, rather than zipping them up.
- This will export compiled Java
[CPP]
Externs now support type parameters.[LUA]
Now includes an implementation of SSL.[JAVA]
Now automatically applies@:java.default
to methods with default implementations while loading JARs.[JAVA]
Experimental support for functional interfaces. Notably, the functional interface does not need to be an extern to a Java functional interface.
// NOTE: This code only works on the Java target.
// A similar result can be achieved on other targets by using abstracts.
interface MathOperation {
function perform(a:Int, b:Int):Int;
}
class Ops {
static public final add:MathOperation = (a, b) -> a + b;
static public final subtract:MathOperation = (a, b) -> a - b;
static public function performMathOperation(operation:MathOperation) {
return operation.perform(8, 4);
}
}
class Main {
static function main() {
var result = Ops.performMathOperation(Ops.add);
trace('Add: ${result}');
result = Ops.performMathOperation(Ops.subtract);
trace('Subtract: ${result}');
result = Ops.performMathOperation(multiply);
trace('Multiply: ${result}');
result = Ops.performMathOperation(function(a, b):Int {
return Std.int(a / b);
});
trace('Divide: ${result}');
}
static function multiply(a, b):Int {
return a * b;
}
}
Bug Fixes and Improvements (All Targets)
I can’t cover all of the bug fixes and optimizations, as some of them are too boring or too obtuse for me, but I’ll try to cover some of them.
- There have been a bunch of optimizations that should result in cleaner, faster code for each compilation target.
- There have been a bunch of bug fixes across different targets.
- Haxe will now guess the return type of a getter function based on the type of the property. Try Haxe
- Haxe now assumes
Null<T>
for nullable values. This is useful when type safety is enabled. Try Haxe - Trying to add a
dynamic
function to anabstract
now fails at compile time instead of runtime, and gives a more helpful error message. Try Haxe