This guide documents small but important conventions Reflaxe.Elixir applies so the generated Elixir:
- is idiomatic and readable
- avoids common compiler warnings
- preserves Haxe semantics in an immutable, expression-oriented target
If you’re looking for construct-by-construct mappings (classes, enums, types, control flow), also see:
docs/02-user-guide/HAXE_ELIXIR_MAPPINGS.md.
Haxe typically uses camelCase. Elixir idiomatically uses snake_case.
Reflaxe.Elixir normalizes:
- local variables and function parameters →
snake_case - function names →
snake_case
Example:
public static function parseNumber(input: String): Int { ... }Generates (shape):
def parse_number(input) do
...
endElixir has reserved keywords like when, end, do, fn, case, etc.
If a generated identifier would collide with a reserved keyword, Reflaxe.Elixir appends a trailing underscore:
when→when_`end`(Haxe escaped identifier) →end_
This keeps the output valid Elixir without requiring awkward names in Haxe.
Example (Haxe):
var when = 1;
var `end` = 2;Generated (shape):
when_ = 1
end_ = 2Sometimes two different Haxe names normalize to the same snake_case identifier (for example userID and user_id,
or HTTPServer and HttpServer).
When that would produce invalid Elixir (shadowing/collision in the same scope), the compiler will disambiguate names in a predictable way. You may see suffixes or other small adjustments in generated code to keep every binding unique.
Tip: if you care about “perfectly clean” output, avoid defining two identifiers that normalize to the same
snake_case name in the same scope.
Haxe packages and class names become Elixir module namespaces:
my.app.UserService→My.App.UserServiceMyThingstaysMyThing(module casing is preserved/normalized)
Use @:native("My.App.UserService") when you need an exact module name.
Elixir warns on unused variables and parameters. The idiomatic way to silence this is a leading underscore (_var).
Reflaxe.Elixir performs usage analysis and automatically prefixes unused binders in the generated Elixir:
- unused local variables →
_var_name = ... - unused function/callback parameters →
def f(_arg) do ... end - unused pattern binders:
case/withthrowaway slots prefer wildcard_when safe- binders that carry pattern semantics stay named (
_name)
This means:
- You do not need to write leading underscores in Haxe to get warning-free Elixir.
- You can still use
_fooin Haxe to explicitly communicate “intentionally unused”; the compiler preserves it.
Example (Haxe):
var upperResult = ResultTools.map(result, s -> s.toUpperCase());
// upperResult intentionally unusedGenerated (shape):
_upper_result = ResultTools.map(result, fn s -> String.upcase(s) end)_is treated as the Elixir wildcard and is preserved as-is._nameis treated as an intentionally-unused named variable and is preserved (or generated) as_name.
Generated policy for readability + semantics:
- Function/callback parameters use
_nameso signatures stay easy to read. - Pattern slots in
case/with/receiveuse_when that slot is a pure discard. - The compiler keeps
_namein patterns when wildcarding would change matching behavior (for example repeated binders or alias binders).
Example:
switch (result) {
case Ok(value): true;
case Error(reason): false;
}case result do
{:ok, _} -> true
{:error, _} -> false
endWhen a named binder is required to keep pattern behavior (for example repeated binders that must match),
generated code keeps an underscored name such as {:pair, _x, _x}.
Phoenix function components and ~H templates expect the parameter to be named assigns.
Even if it’s unused, the compiler keeps it as assigns (it will not be rewritten to _assigns).
Reflaxe.Elixir represents Haxe enums as tagged tuples, so pattern matching is uniform:
- zero-argument constructor →
{:red}(1-tuple) - constructor with N args →
{:rgb, r, g, b}(N+1 tuple)
Example (Haxe):
enum Color {
Red;
Rgb(r:Int, g:Int, b:Int);
}Generated usage (shape):
{:red}
{:rgb, r, g, b}Two enums are used pervasively in the stdlib and Phoenix surfaces:
Option<T>:Some(v)→{:some, v}None→{:none}
Result<T, E>:Ok(v)→{:ok, v}Error(e)→{:error, e}
These shapes are intentionally “Elixir-native”:
- they work well with
case, guards, and pipelines - they’re compatible with typical OTP-style return conventions
null in Haxe becomes nil in Elixir. Prefer Option<T> when you want the type system to force handling.
Array<T> compiles to an Elixir list ([...]). Many familiar operations become Enum.* calls:
array.map(f)→Enum.map(array, f)array.filter(f)→Enum.filter(array, f)array.contains(x)→Enum.member?(array, x)
This is implemented in std/Array.cross.hx and is designed to read like hand-written Elixir.
Haxe anonymous structures / typedef “records” compile to Elixir maps with atom keys:
var user = { name: "Alice", age: 42 };Shape:
user = %{:name => "Alice", :age => 42}Field reads and updates use idiomatic Elixir map syntax:
user.name(read)%{user | name: "Bob"}(update)
Reflaxe.Elixir models “instances” as immutable map-backed values. Instance methods become module functions that take
the instance as an explicit first parameter (often named struct in generated code).
Shape:
defmodule Point do
def new(x_param, y_param) do
struct = %{:x => nil, :y => nil}
struct = %{struct | x: x_param}
struct = %{struct | y: y_param}
struct
end
def distance(struct, other) do
...
end
endIf your Haxe code relies heavily on mutating fields, consider refactoring toward returning updated values (functional style), which maps naturally onto Elixir.
Haxe-style field assignment compiles to immutable map update syntax.
Example (shape):
struct = %{struct | x: new_x}- Haxe anonymous structures compile to maps with atom keys (e.g.
%{:name => "Alice"}). - If you need string keys (common for JSON-ish payloads), use an explicit
Map<String, T>/ dynamic map and the generated code will use string keys where appropriate.
Many Elixir APIs use ? / ! suffixes (member?, fetch!, get_in, etc.). Those aren’t valid Haxe identifiers.
Use @:native on externs to call them precisely:
extern class Enum {
@:native("member?")
static function member<T>(list:Array<T>, value:T):Bool;
}This keeps your Haxe code typed and avoids raw Elixir injection.
Example (! function):
import elixir.ElixirMap;
// Calls `Map.fetch!/2` under the hood.
var value = ElixirMap.fetchBang(myMap, key);This section explains a generated-code detail that comes from ordinary Haxe comparisons against an already-known local.
Example Haxe source:
var expectedStatus = status;
var label = (status == expectedStatus) ? "same" : "other";When lowered to expression-oriented Elixir, a common shape is a case comparison with pinning.
In Elixir patterns, ^name means "match the existing value", while plain name creates/rebinds a pattern variable.
During lowering, you may see a shadow-prone intermediate form that is normalized to the final safe shape:
# Plain pattern variable: binds a new value in pattern position
expected_status = status
case status do
expected_status -> :same
_ -> :other
end
# Compiler-emitted shape: pin matches the existing local
expected_status = status
case status do
^expected_status -> :same
_ -> :other
end^expected_status preserves the comparison intent from Haxe. Without ^, the branch pattern would bind a new value.
Reflaxe.Elixir runs late hygiene passes to pin existing bindings where needed, so generated pattern code preserves intent and avoids accidental shadowing.
If you want strict "assign once" behavior in Haxe source, use final (enforced by Haxe typing). There is currently no
global Reflaxe.Elixir mode that forbids all rebinding in generated Elixir.
When the compiler sees a contiguous sequence like x = f(x, ...) then x = g(x, ...), it may collapse it into a pipe:
x = x |> f(...) |> g(...)This is a readability optimization only; it doesn’t change semantics.
Haxe switch is an expression; Elixir case is also an expression, but compilation sometimes needs
temporary variables to preserve evaluation order and “return from inside” behavior.
Example Haxe source that may trigger helper shapes:
var label = switch (status) {
case Pending: "waiting";
case Processing(progress): 'processing ${progress}%';
case Done(result): result;
}Why this exists: the compiler must preserve Haxe expression semantics exactly, even when Elixir needs extra plumbing bindings to represent the same flow safely.
You may see compiler-generated helpers such as:
_g/gscratch variables (for expression plumbing)Enum.reduce_while(..., :__reflaxe_no_return__, fn ... -> ... end)patterns{:__reflaxe_return__, value}sentinels when areturnmust escape an internal loop
These are intentional and exist to keep Haxe semantics correct in Elixir’s immutable model.
When Haxe uses break / continue, compilation may introduce throw/catch inside
Enum.reduce_while to emulate structured loop control flow without mutable state.
Example Haxe source:
var found = -1;
for (i in 0...items.length) {
if (items[i] == null) continue;
if (items[i] == target) {
found = i;
break;
}
}Why this exists: Elixir has no direct mutable loop + break/continue construct, so the compiler encodes
those exits with reduction state and structured throws/catches.
In those cases you’ll often see patterns like:
Enum.reduce_while(..., acc, fn _, acc -> ... end)throw({:break, acc})/throw({:continue, acc})catch :throw, {:break, break_state} -> ...
Haxe static var is mutable. To preserve semantics, static storage is implemented using process-local storage
(you may see Process.get/put in generated code).
This is correct for “compiler/runtime needs”, but for application state prefer BEAM-native patterns (GenServer state, LiveView assigns, ETS, etc.).
When you need exact Elixir code, use the supported injection surfaces (documented here):
docs/04-api-reference/ELIXIR_INJECTION_GUIDE.md
These are powerful but should be used sparingly; prefer typed externs and stdlib helpers when possible.