Station X1 · Bilingual extensionOpen the adventure map
Agree on shared BEAM terms
Elixir and Erlang spell things differently. Stable terms let them keep one contract.
When an order receives pay, ship, cancel, and duplicate events, how can Elixir and Erlang follow the same state-machine protocol?
The same paid value is not the same state on both sides
One side uses an atom while the other sends a string. One implementation permits a transition that the other rejects. After a duplicate event, the two copies of the order state diverge.
- The complete state-transition table
- The raw terms exchanged across the language boundary
- Bilingual contract tests for valid, invalid, and duplicate events
The problem it solves
Comparing the same task in both languages is clearer than memorizing each one alone. Elixir writes :ok; Erlang writes ok. On BEAM, they are the same atom. Strings, modules, and exceptions still have real differences.
You will be able to
Translate common data, functions, and module calls from Elixir syntax to Erlang syntax and back
Explain which differences are only surface spelling and which values are already the same BEAM term
Use specs to record an agreed data shape and tests to catch changes to that agreement
- Finish Foundation in either language. You can read the other language beside it as you go
- Know how to check that the same input produces the same output
See what it does before how it is written
Both sides receive the same atoms and return success or error tuples with the same shapes.
# List the allowed order states as atoms
defmodule Order do
@type state :: :new | :paid | :shipped
# Return a new state on success and a clear error tag on failure
@spec transition(state(), atom()) ::
{:ok, state()} | {:error, :invalid_transition}
def transition(:new, :pay), do: {:ok, :paid}
def transition(:paid, :ship), do: {:ok, :shipped}
def transition(_, _), do: {:error, :invalid_transition}
endWhy this shape?
Why align terms before making cross-language calls?
Elixir and Erlang share BEAM terms. Agree on atoms, tuples, binaries, and error results before crossing the boundary.
A shared VM does not make structs, records, and text conventions automatically compatible.
Coming from Java, Python, or JavaScript
Java
- Familiar starting point
- Two JVM languages sharing bytecode and an object model.
- What BEAM changes
- Elixir and Erlang share BEAM terms and module/function/arity, while surface conventions can still differ.
- False friend
- A record and struct are not one public DTO.
Python
- Familiar starting point
- Modules passing dicts, tuples, and bytes directly.
- What BEAM changes
- Shared terms make calls direct, while patterns make boundary shapes explicit.
- False friend
- A charlist and binary can both look textual but are different terms.
JavaScript
- Familiar starting point
- Modules sharing objects or exchanging JSON.
- What BEAM changes
- Code in one VM needs no JSON step, but it still needs a stable term contract.
- False friend
- The ability to pass internals is not a reason to expose them.
Pass the same data both ways
Call both modules in both directions and compare atoms and tuples after they cross the language boundary.
- 01
Put
order.erlin the Mix project'ssrc/folder and compile the project - 02
In IEx, call
:order.transition(:new, :pay)and record the return value - 03
From Erlang, call
'Elixir.Order':transition(new, pay)and compare the data shapes
# Compile the current Mix project and open IEx in its environment
iex -S mix- Both sides receive the same tuple term represented by
{ok, paid} - Erlang can find and call an Elixir module by using its real module atom
Change the atom paid on one side to the string "paid". The text looks alike, but the term type differs, so matching fails.
Ordinary BEAM terms such as numbers, atoms, and tuples can pass directly between modules in the two languages.
Passing ordinary terms does not mean strings, Elixir structs, Erlang records, and exceptions need no extra agreement.
Key ideas in this code
Atom mapping
An atom is a fixed label. Elixir's :ok and Erlang's ok are the same atom. An Elixir module name is also an atom beginning with Elixir..
Module call
A call is identified by its module name, function name, and number of arguments. That number is called arity.
Agreed data format
Two modules agree on the shapes of inputs, returns, and errors. Across languages, these agreements matter more than surface spelling.
term
A piece of BEAM data is a term: a number, atom, tuple, list, map, and more. Ordinary terms can pass directly between both languages.
Design patterns used here
State Machine
Define the allowed next state from the current state and event.
Command
Represent each pay, ship, or cancel event as an explicit command term.
Contract Test
Run both implementations against the same transition table.
Which real module name does Erlang usually use to find the Elixir module Foo?
An order-state relay
Add cancellation, refunds, and repeated events. Draw the state-transition table first, then implement it in both languages.
Hint 1Take the first step
Draw the states and events before writing code.
Hint 2Make it a little smaller
Decide whether a repeated event keeps the same state or returns an error, and write down the idempotency rule.
Hint 3You are close
Use pure functions to make the rules clear before hiding an unfinished idea inside a process.
Hint 4Work backward from the finish
Choose one success signal and write the smallest test for it. If the computer cannot show the result, rewrite the signal as something you can truly observe.
Both implementations return terms with the same shapes
A forbidden state change returns a clear error tag
Specs and tests cover every path in the state table
Remember three things
- 1
Agree on input and return term shapes before choosing a language to express them.
- 2
Elixir module names and aliases still become module atoms when they reach BEAM.
- 3
Text, structs, records, and exceptions are the easiest places for the two sides to misunderstand each other. Agree and test them clearly.