Complete guide to how Haxe constructs map to Elixir code
- Overview
- Core Language Mappings
- Type System Mappings
- Annotation-Driven Transformations
- Function and Method Mappings
- Control Flow Mappings
- Pattern Matching
- Cross-Platform Type Safety
- Ergonomic Features
- Migration Patterns
Reflaxe.Elixir transforms idiomatic Haxe code into idiomatic Elixir code. This document provides a comprehensive reference for how each Haxe construct maps to its Elixir equivalent.
- Predictable Mappings: Similar Haxe constructs produce similar Elixir patterns
- Idiomatic Output: Generated Elixir follows BEAM/OTP conventions
- Type Safety Preservation: Compile-time safety translates to runtime correctness
- Annotation-Driven Specialization: Use annotations to override defaults for specialized patterns
Default Behavior: All Haxe classes become Elixir modules.
Haxe Input:
class UserService {
public static function create(name: String): User {
return new User(name);
}
public function greet(user: User): String {
return 'Hello, ${user.name}!';
}
}Generated Elixir:
defmodule UserService do
@doc """
UserService module generated from Haxe
"""
@spec create(String.t()) :: User.t()
def create(name) do
User.new(name)
end
@spec greet(User.t()) :: String.t()
def greet(user) do
"Hello, #{user.name}!"
end
endKey Transformations:
- Class name → Module name
- Static methods →
deffunctions - Instance methods →
deffunctions (with implied context) - Public visibility → Public functions
- Private visibility →
defpfunctions
Reflaxe.Elixir represents Haxe enums as tagged tuples in Elixir, even for constructors with no arguments. This keeps pattern matching uniform and avoids ambiguity between enum values and “plain” atoms used elsewhere.
Zero-argument constructors → 1-tuples:
Haxe Input:
enum Color {
Red;
Green;
Blue;
}Generated Elixir:
# Usage generates 1-tuples: {:red}, {:green}, {:blue}Enums with Data → Tagged Tuples (2+ elements):
Haxe Input:
enum Result<T, E> {
Ok(value: T);
Error(error: E);
}Generated Elixir:
# Usage generates: {:ok, value}, {:error, error}Complex Enums → Tagged Tuples with Multiple Fields:
Haxe Input:
enum Shape {
Circle(radius: Float);
Rectangle(width: Float, height: Float);
Triangle(a: Float, b: Float, c: Float);
}Generated Elixir:
# Usage generates:
# {:circle, radius}
# {:rectangle, width, height}
# {:triangle, a, b, c}Default: Interfaces become Elixir Protocols for polymorphic behavior.
Haxe Input:
interface Drawable {
function draw(): String;
function area(): Float;
}Generated Elixir:
defprotocol Drawable do
@doc "Draw the shape"
@spec draw(t()) :: String.t()
def draw(shape)
@doc "Calculate the area"
@spec area(t()) :: float()
def area(shape)
endType Aliases:
Haxe Input:
typedef UserId = Int;
typedef UserName = String;Generated Elixir:
@type user_id :: integer()
@type user_name :: String.t()Structural Types:
Haxe Input:
typedef User = {
id: Int,
name: String,
?email: String,
active: Bool
}Generated Elixir:
@type user :: %{
id: integer(),
name: String.t(),
email: String.t() | nil,
active: boolean()
}| Haxe Type | Elixir Type | Notes |
|---|---|---|
Int |
integer() |
Arbitrary precision integers |
Float |
float() |
IEEE 754 double precision |
String |
String.t() |
UTF-8 binary strings |
Bool |
boolean() |
true or false atoms |
Array<T> |
list(T) |
Immutable linked lists |
Map<K,V> |
map(K, V) |
Immutable hash maps (see note below) |
Dynamic |
term() |
Any Elixir term |
Void |
:ok or nil |
Context dependent |
Null<T> |
T | nil |
Nullable types |
Option<T> |
{:some, T} | {:none} |
Type-safe null handling |
Result<T,E> |
{:ok, T} | {:error, E} |
Explicit error handling |
In Elixir, many framework APIs (Phoenix params, Presence.list/2, JSON payloads, etc.) return native maps
(%{}) as plain runtime terms.
Those values are not guaranteed to be a Haxe Map<K,V> runtime object, even if they are “map-like”.
When dealing with boundary terms, prefer Elixir-native helpers like elixir.ElixirMap.get/3 (and typed
decode helpers such as WirePayload) instead of calling Haxe Map instance methods.
Option Pattern:
// Haxe
var user: Option<User> = findUser(123);
switch (user) {
case Some(u): processUser(u);
case None: handleNotFound();
}# Generated Elixir
user = find_user(123)
case user do
{:some, u} -> process_user(u)
{:none} -> handle_not_found()
endFor practical guidance on writing Haxe that compiles into clean Elixir, see:
docs/02-user-guide/WRITING_IDIOMATIC_HAXE_FOR_ELIXIR.mddocs/02-user-guide/ELIXIR_IDIOMS_AND_HYGIENE.md
Result<T,E> Pattern:
// Haxe
var result: Result<User, String> = validateUser(data);
switch (result) {
case Ok(user): createUser(user);
case Error(msg): logError(msg);
}# Generated Elixir
result = validate_user(data)
case result do
{:ok, user} -> create_user(user)
{:error, msg} -> log_error(msg)
endAnnotations override default class→module mapping for specialized Elixir patterns:
| Annotation | Generated Module Type | Primary Use Case |
|---|---|---|
@:module |
Plain module with functions | Utility functions, stateless services |
@:struct |
Module with defstruct |
Data containers, DTOs |
@:genserver |
OTP GenServer | Stateful processes, caches |
@:liveview |
Phoenix LiveView | Real-time UI components |
@:controller |
Phoenix Controller | HTTP request handlers |
@:router |
Phoenix Router | Request routing logic |
@:channel |
Phoenix Channel | WebSocket handling |
@:schema |
Ecto Schema | Database models |
@:changeset |
Ecto Changeset | Data validation |
@:protocol |
Elixir Protocol | Polymorphic behavior |
@:behaviour |
Elixir Behaviour | Callback contracts |
@:supervisor |
OTP Supervisor | Process supervision |
@:application |
OTP Application | Application entry point |
Haxe Input:
@:genserver
class Counter {
private var count: Int = 0;
@:call
public function get(): Int {
return count;
}
@:cast
public function increment(): Void {
count++;
}
}Generated Elixir:
defmodule Counter do
use GenServer
# Client API
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, :ok, opts)
end
def get(pid) do
GenServer.call(pid, :get)
end
def increment(pid) do
GenServer.cast(pid, :increment)
end
# Server Callbacks
@impl true
def init(:ok) do
{:ok, %{count: 0}}
end
@impl true
def handle_call(:get, _from, %{count: count} = state) do
{:reply, count, state}
end
@impl true
def handle_cast(:increment, %{count: count} = state) do
{:noreply, %{state | count: count + 1}}
end
endHaxe Input:
class MathUtils {
public static function add(a: Int, b: Int): Int {
return a + b;
}
private static function multiply(a: Int, b: Int): Int {
return a * b;
}
}Generated Elixir:
defmodule MathUtils do
@spec add(integer(), integer()) :: integer()
def add(a, b) do
a + b
end
@spec multiply(integer(), integer()) :: integer()
defp multiply(a, b) do
a * b
end
endHaxe Input:
class Calculator {
private var memory: Float = 0;
public function add(value: Float): Float {
memory += value;
return memory;
}
public function getMemory(): Float {
return memory;
}
}Generated Elixir:
defmodule Calculator do
defstruct [:memory]
def new() do
%Calculator{memory: 0}
end
def add(%Calculator{memory: memory} = calc, value) do
new_memory = memory + value
{new_memory, %{calc | memory: new_memory}}
end
def get_memory(%Calculator{memory: memory}) do
memory
end
endHaxe Input:
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map(x -> x * 2);
var filtered = numbers.filter(function(x) return x > 3);Generated Elixir:
numbers = [1, 2, 3, 4, 5]
doubled = Enum.map(numbers, fn x -> x * 2 end)
filtered = Enum.filter(numbers, fn x -> x > 3 end)Haxe Input:
function processStatus(status: Status): String {
return switch (status) {
case Pending: "Waiting";
case Processing(progress): 'In progress: ${progress}%';
case Completed(result): 'Done: ${result}';
case Failed(error): 'Error: ${error}';
}
}Generated Elixir:
def process_status(status) do
case status do
:pending -> "Waiting"
{:processing, progress} -> "In progress: #{progress}%"
{:completed, result} -> "Done: #{result}"
{:failed, error} -> "Error: #{error}"
end
endFor Loops → Enum Operations:
Haxe Input:
var total = 0;
for (item in items) {
total += item.value;
}Generated Elixir:
total = Enum.reduce(items, 0, fn item, acc -> acc + item.value end)While Loops → Recursive Functions:
Haxe Input:
var count = 0;
while (count < 10) {
trace(count);
count++;
}Generated Elixir:
(fn loop_fn ->
count = 0
if count < 10 do
IO.inspect(count)
count = count + 1
loop_fn.(loop_fn)
end
end).(fn f -> f.(f) end)Haxe Input:
var message = if (user.isActive) {
"Welcome back!";
} else {
"Please activate your account";
}Generated Elixir:
message = if user.is_active do
"Welcome back!"
else
"Please activate your account"
endHaxe Input:
var {name, age} = user;
var [first, second, ...rest] = items;Generated Elixir:
%{name: name, age: age} = user
[first, second | rest] = itemsThis detail matters when Haxe logic compares a value against a local that was computed earlier.
Haxe Input (intent):
var expected = status;
var label = (status == expected) ? "same" : "other";Generated Elixir (one common shape):
expected = status
label =
case status do
^expected -> "same"
_ -> "other"
endThe ^ is Elixir's pin operator: it means "compare against the existing value in expected".
Without ^, expected in a pattern would bind a new value instead of comparing.
Reflaxe.Elixir applies late pattern-hygiene passes so generated case/with code keeps the original Haxe
comparison intent and does not accidentally shadow prior locals.
For the fuller discussion, see
docs/02-user-guide/ELIXIR_IDIOMS_AND_HYGIENE.md#binding-semantics-in-generated-patterns.
Haxe Input:
function handleResponse(response: Response): String {
return switch (response) {
case Success({data: userData, status: 200}):
'User: ${userData.name}';
case Success({status: code}) if (code >= 200 && code < 300):
"Success with code " + code;
case Error({message: msg, code: 404}):
"Not found: " + msg;
case Error({message: msg}):
"Error: " + msg;
case _:
"Unknown response";
}
}Generated Elixir:
def handle_response(response) do
case response do
{:success, %{data: user_data, status: 200}} ->
"User: #{user_data.name}"
{:success, %{status: code}} when code >= 200 and code < 300 ->
"Success with code #{code}"
{:error, %{message: msg, code: 404}} ->
"Not found: #{msg}"
{:error, %{message: msg}} ->
"Error: #{msg}"
_ ->
"Unknown response"
end
endWorks consistently across all Haxe targets while generating platform-optimal code:
Haxe Source:
import haxe.ds.Option;
using haxe.ds.OptionTools;
function findUser(id: Int): Option<User> {
var user = database.query("users", {id: id});
return user != null ? Some(user) : None;
}
function processUser(id: Int): String {
return findUser(id)
.map(user -> user.name)
.filter(name -> name.length > 0)
.unwrap("Anonymous");
}Elixir Compilation:
def find_user(id) do
user = Database.query("users", %{id: id})
if user != nil do
{:some, user}
else
{:none}
end
end
def process_user(id) do
case find_user(id) do
{:some, user} when byte_size(user.name) > 0 -> user.name
_ -> "Anonymous"
end
endHaxe Source:
import haxe.functional.Result;
using haxe.functional.ResultTools;
function validateUser(data: UserData): Result<User, ValidationError> {
return validateEmail(data.email)
.flatMap(_ -> validateAge(data.age))
.map(age -> new User(data.email, age));
}
function processUsers(dataList: Array<UserData>): Result<Array<User>, ValidationError> {
return ResultTools.traverse(dataList, validateUser);
}Elixir Compilation:
def validate_user(data) do
case validate_email(data.email) do
{:ok, _} ->
case validate_age(data.age) do
{:ok, age} -> {:ok, User.new(data.email, age)}
{:error, reason} -> {:error, reason}
end
{:error, reason} -> {:error, reason}
end
end
def process_users(data_list) do
Enum.reduce_while(data_list, {:ok, []}, fn data, {:ok, acc} ->
case validate_user(data) do
{:ok, user} -> {:cont, {:ok, [user | acc]}}
{:error, reason} -> {:halt, {:error, reason}}
end
end)
|> case do
{:ok, users} -> {:ok, Enum.reverse(users)}
{:error, reason} -> {:error, reason}
end
endThe compiler can detect common patterns and optimize accordingly:
Reassignment Pipelines → Pipe Operators:
// Haxe input
var result = items;
result = result.filter(x -> x > 2);
result = result.map(x -> x * 2);# Generated Elixir (optimized shape)
result = items
result = result |> Enum.filter(fn x -> x > 2 end) |> Enum.map(fn x -> x * 2 end)This optimization targets contiguous assignments where the variable is passed as the first argument
(x = f(x, ...)), and collapses them into a single |> pipeline.
Classes → Module + map-backed instances:
// Haxe input
class Point {
public var x: Float;
public var y: Float;
public function new(x: Float, y: Float) {
this.x = x;
this.y = y;
}
}# Generated Elixir (current 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
endInstance methods become module functions that take the instance as an explicit first parameter (often
named struct in the generated Elixir).
The compiler optimizes array operations to use appropriate Elixir functions:
Haxe Input:
var numbers = [1, 2, 3, 4, 5];
var doubled = numbers.map(x -> x * 2);
var filtered = numbers.filter(x -> x > 3);
var total = numbers.reduce((a, b) -> a + b);
var length = numbers.length;
var contains = numbers.contains(3);Generated Elixir:
numbers = [1, 2, 3, 4, 5]
doubled = Enum.map(numbers, fn x -> x * 2 end)
filtered = Enum.filter(numbers, fn x -> x > 3 end)
total = Enum.reduce(numbers, fn a, b -> a + b end)
length = length(numbers)
contains = Enum.member?(numbers, 3)Phase 1: Identify Nullable APIs
// Current nullable approach
function findUser(id: Int): Null<User> {
// Implementation
}
// Usage requires null checks
var user = findUser(123);
if (user != null) {
processUser(user);
}Phase 2: Migrate to Option
// Migrated to Option
function findUser(id: Int): Option<User> {
var user = database.find(id);
return user != null ? Some(user) : None;
}
// Type-safe usage
switch (findUser(123)) {
case Some(user): processUser(user);
case None: handleNotFound();
}Phase 3: Leverage Functional Operations
// Functional style
function getUserEmail(id: Int): String {
return findUser(id)
.map(user -> user.email)
.filter(email -> email != "")
.unwrap("no-email@example.com");
}Phase 1: Exception-Based Approach
function processPayment(amount: Float): Transaction {
try {
var validation = validateAmount(amount);
var payment = chargeCard(amount);
return createTransaction(payment);
} catch (e: PaymentError) {
throw e; // Error information can be lost
}
}Phase 2: Result-Based Approach
function processPayment(amount: Float): Result<Transaction, PaymentError> {
return validateAmount(amount)
.flatMap(validAmount -> chargeCard(validAmount))
.map(payment -> createTransaction(payment));
}
// Caller must handle both cases
switch (processPayment(100.0)) {
case Ok(transaction): completeOrder(transaction);
case Error(error): handlePaymentError(error);
}- Functional Patterns - Examples of imperative→functional transformations
- Annotations Reference - Complete annotation documentation
- Compiler Best Practices - Patterns and conventions for compiler/std development
- ExUnit Testing Guide - Testing patterns for mapped constructs
- Elixir Language Reference
- OTP Design Principles
- Gleam Language - Inspiration for type-safe BEAM patterns
- Phoenix Framework - Web framework patterns