PONY λ M2 Modula-2

Erlang.CodeCompared.To/Pony

An interactive executable cheatsheet comparing Erlang and Pony

Erlang/OTP 26 Pony 0.63
Output & Running
Hello, World
io:format("Hello, World!~n").
actor Main new create(env: Env) => env.out.print("Hello, World!")
Erlang prints with io:format/1 and the ~n directive for the newline. Every Pony program's entry point is actor Main with a new create(env: Env) constructor — env.out.print appends the newline for you, the way io:format's ~n does explicitly.
Printing a value for inspection
Value = {ok, 42}, io:format("~p~n", [Value]).
actor Main new create(env: Env) => let value: I64 = 42 env.out.print("(ok, " + value.string() + ")")
Erlang's ~p directive renders any term for humans automatically. Pony has no universal "pretty print" — every value must be explicitly converted with .string() and concatenated by hand, since Pony has no runtime reflection over arbitrary types the way Erlang's term representation allows.
Dynamic vs. Static Types
Runtime checking vs. compile-time checking
% No type declaration anywhere — this only fails when it actually RUNS: Add = fun(X, Y) -> X + Y end, io:format("~p~n", [Add(2, 3)]).
primitive Adder fun apply(x: I64, y: I64): I64 => x + y actor Main new create(env: Env) => env.out.print(Adder(2, 3).string())
Erlang has no compile-time type checking at all. Pony is fully statically typed — every parameter and return type is checked before the program ever runs, and Pony additionally checks that every reference capability is used safely, catching entire categories of concurrency bugs Erlang can only ever discover at runtime.
The Headline Parallel: Actors
Actors are not a library — they are the language
Pid = spawn(fun() -> io:format("running in a process~n") end), io:format("~p~n", [is_pid(Pid)]).
actor Greeter be greet() => None // a real program would print here — see the next example actor Main new create(env: Env) => let greeter = Greeter greeter.greet() env.out.print("spawned a Greeter actor")
This is the strongest headline parallel on the whole site: both languages put the actor at the absolute center of the language, not layered on top as a library the way Scala's Akka or F#'s MailboxProcessor are. Erlang's spawn/1 creates a process from a fun; Pony's actor Greeter declares an actor type directly, and let greeter = Greeter instantiates one — conceptually the same act of bringing a new independent unit of concurrency into existence.
One message at a time, in order — in both languages
self() ! first, self() ! second, receive first -> io:format("first~n") end, receive second -> io:format("second~n") end.
actor Counter var _count: I64 = 0 be increment() => _count = _count + 1 be show(env: Env) => env.out.print(_count.string()) actor Main new create(env: Env) => let counter = Counter counter.increment() counter.increment() counter.show(env)
Both an Erlang process and a Pony actor process exactly one message at a time, strictly in the order received — this is precisely what makes _count safe to mutate directly inside Counter without any lock, the same way an Erlang process's internal state never needs a mutex. Two increment() calls can never interleave inside one actor.
be vs. fun: Async vs. Sync
`be` is Pony's `!` — fire-and-forget, by the type system
self() ! {log, "hello"}, receive {log, Message} -> io:format("~s~n", [Message]) end.
actor Logger be log(env: Env, message: String) => env.out.print(message) actor Main new create(env: Env) => let logger = Logger logger.log(env, "hello") // sends a message — returns immediately
Calling logger.log(env, "hello") does not run log synchronously — it enqueues a message in Logger's mailbox and returns to the caller immediately, exactly like Erlang's !. The difference is that Pony makes this explicit in the type system: any method declared be is guaranteed async and returns None, where Erlang's ! is just an operator you apply to any Pid, with no type-level distinction from a synchronous call.
`fun` is an ordinary synchronous call — like calling within one Erlang process
Double = fun(X) -> X * 2 end, io:format("~p~n", [Double(21)]).
primitive Doubler fun apply(x: I64): I64 => x * 2 actor Main new create(env: Env) => env.out.print(Doubler(21).string())
A Pony fun is an ordinary, synchronous, value-returning call — the caller waits and gets a result directly, just like calling an Erlang fun from within the same process. Erlang has no separate "async function" concept: everything inside one process is synchronous, and the only asynchronous operation is sending a message with ! to a different process.
Per-Actor GC: A Shared Trait
Both give every concurrent unit its own private, paused-free heap
% Erlang: each process has its OWN heap, garbage-collected % independently — one process's GC pause never affects any other: List = [1, 2, 3], io:format("~p~n", [List]).
actor Main new create(env: Env) => // Pony's runtime interleaves GC with program execution, actor by // actor — no other actor is ever paused by one actor's collection: let numbers: Array[I64] = [1; 2; 3] env.out.print(numbers.size().string())
This is a genuine shared design decision, not just superficially similar wording: both languages give every concurrent unit (Erlang process / Pony actor) its own private heap, collected independently. Neither language has a global "stop the world" garbage collector pause — one actor's or process's collection never blocks any other, a guarantee most garbage-collected languages (Go, the JVM, .NET) cannot make in the same way.
Reference Capabilities
Erlang copies; Pony proves sharing is safe instead
% Erlang: sending a message to another process COPIES the data — % the sender and receiver never share the same memory: self() ! {ok, [1, 2, 3]}, receive {ok, List} -> io:format("~p~n", [List]) end.
actor Main new create(env: Env) => let numbers: Array[I64] val = [1; 2; 3] // "val" here is a reference capability: a GLOBALLY IMMUTABLE // reference, safe to pass to another actor with NO COPY at all — // Pony's compiler proves no one can ever mutate it afterward. env.out.print(numbers.size().string())
This is the biggest new concept for an Erlang developer, and the deepest technical difference between the two languages. Erlang guarantees safety by always copying data across a process boundary — there is genuinely no other memory to worry about. Pony instead uses reference capabilities (iso, val, ref, box, tag, trn) to let the compiler prove, at compile time, that a given reference can be safely handed to another actor with zero copying — val above means "globally immutable, therefore safe to share," so Pony can pass even huge structures between actors instantly, something Erlang's copy-everything model cannot do without cost proportional to size.
`iso`: unique, transferable ownership
% Erlang has no equivalent — a value can be referenced from as % many places as you like, simultaneously, forever: List = [1, 2, 3], Copy = List, io:format("~p and ~p~n", [List, Copy]).
actor Main new create(env: Env) => let numbers: Array[I64] iso = [1; 2; 3] // "iso" means UNIQUELY owned — no other reference to this array // can exist anywhere else in the whole program at the same time. env.out.print(numbers.size().string())
An iso reference is guaranteed to be the only reference to that data anywhere in the program — closer to Rust's ownership model than anything in Erlang. Erlang has no concept of "the only reference" at all; a value can be bound to any number of variables across any number of processes (as long as it was copied to each), with none of them ever becoming invalid.
Pattern Matching
Function clauses become a `match` expression
Describe = fun Describe(0) -> "zero"; Describe(N) when N > 0 -> "positive"; Describe(_) -> "negative" end, io:format("~s~n", [Describe(-5)]).
primitive Describer fun apply(n: I64): String => match n | 0 => "zero" | let n': I64 if n' > 0 => "positive" else "negative" end actor Main new create(env: Env) => env.out.print(Describer(-5))
Erlang picks a function clause by matching the call against each in order; Pony's match expression works similarly, with | introducing each case and a guard condition attached with if. Pony's syntax needs an explicit bound name (let n': I64) to both bind and guard in one arm — slightly more ceremony than Erlang's bare N when N > 0, but the same underlying idea.
Errors: Partial Functions vs. {error, _}
Partial functions (`?`) replace the `{error, Reason}` tuple
Divide = fun(_, 0) -> {error, divide_by_zero}; (X, Y) -> {ok, X div Y} end, io:format("~p~n", [Divide(10, 0)]).
primitive Divider fun apply(x: I64, y: I64): I64 ? => if y == 0 then error end x / y actor Main new create(env: Env) => try env.out.print(Divider(10, 0)?.string()) else env.out.print("crashed: divide by zero") end
Where Erlang wraps failure in a {error, Reason} tuple the caller must pattern-match, Pony marks a function partial with a trailing ? in its signature — fun apply(x: I64, y: I64): I64 ? — meaning it might raise instead of returning. Calling it requires either another ? (propagating the failure up, close to Rust's ? operator) or a try/else block, as shown, to handle it locally.
A Pony error carries no reason — unlike `{error, Reason}`
% Erlang: the reason travels WITH the failure — {error, Reason} = {error, divide_by_zero}, io:format("~p~n", [Reason]).
primitive Divider fun apply(x: I64, y: I64): I64 ? => if y == 0 then error end x / y actor Main new create(env: Env) => try env.out.print(Divider(10, 0)?.string()) else // Pony's "error" carries NO payload — if you need a reason, // you must return it separately (e.g. wrapped in a union type): env.out.print("failed, but no reason is attached") end
This is a real ergonomic gap compared to Erlang: Pony's error is a bare control-flow signal with no attached data at all, where Erlang's {error, Reason} always carries an explanation for free. Modeling "failure with a reason" in Pony means layering your own union type ((I64 | String)) or a small error-data class on top of the partial-function mechanism — there is no single built-in idiom as convenient as Erlang's tagged tuple.
Behaviours vs. Traits
A loose parallel: behaviours and traits both define a contract
% Erlang: a behaviour (like gen_server) declares a set of callbacks % a module must implement — checked loosely, not by the compiler itself: io:format("~p~n", [gen_server]). % just illustrating the module name exists
trait Greetable fun greet(): String class Person is Greetable let _name: String new create(name: String) => _name = name fun greet(): String => "Hello, " + _name + "!" actor Main new create(env: Env) => let person = Person("Ada") env.out.print(person.greet())
Erlang's behaviours (like gen_server) are a loosely-checked convention — a compiler warning at most if a callback is missing, and only relevant once the behaviour is actually used at runtime. Pony traits are fully enforced at compile time: class Person is Greetable without a greet method simply fails to compile. Pony also supports structural typing via interface (no explicit is declaration needed, just matching methods) alongside nominal typing via trait (explicit is required) — a distinction with no Erlang parallel at all, since Erlang has neither.
Collections
Lists vs. Arrays
[Head | Tail] = [1, 2, 3], io:format("~p and ~p~n", [Head, Tail]).
actor Main new create(env: Env) => let numbers: Array[I64] val = [1; 2; 3] try env.out.print(numbers(0)?.string()) end
Erlang lists are singly-linked cons cells with cheap prepend and no efficient random access. Pony's Array[T] is a contiguous, indexable, growable sequence, closer to Go's slice or Rust's Vec — there is no [Head | Tail] destructuring pattern; indexing with numbers(0) is itself a partial operation (hence the trailing ?), since an out-of-bounds index is a genuine runtime error.
Recursion
Factorial
Factorial = fun Factorial(0) -> 1; Factorial(N) when N > 0 -> N * Factorial(N - 1) end, io:format("~p~n", [Factorial(5)]).
primitive Factorial fun apply(n: I64): I64 => if n == 0 then 1 else n * apply(n - 1) end actor Main new create(env: Env) => env.out.print(Factorial(5).string())
The base case and recursive case survive intact, expressed as an if/else inside one primitive function rather than as separate clauses — Pony, like Go and Clojure, dispatches on a single function body rather than choosing among clauses by argument shape.
No Built-In Supervision Tree
No OTP-equivalent supervision tree
% Erlang: OTP's supervisor behaviour restarts a crashed child process % automatically, according to a declared restart strategy — a first-class % part of the standard library: io:format("~p~n", [supervisor]). % the module name — just illustrating it exists
actor Main new create(env: Env) => // Pony has no standard-library concept playing this role. An actor // that errors inside a behavior simply stops; restarting it (if you // want that at all) is something you design and build by hand: env.out.print("no built-in supervisor")
OTP's supervisor behaviour — declaring which children to restart, how often, and under what strategy — is a first-class, battle-tested part of Erlang's standard library. Pony ships nothing playing this role: an actor that errors simply stops, and automatic recovery is something you would have to design and implement yourself. This is the same gap Go has relative to Erlang, and it is a real one — Pony's compile-time safety guarantees reduce how often you need recovery, but do not replace OTP's answer for what to do when recovery is still needed.
Gotchas for Erlang Developers
No null, no exceptions — only partial functions
% Erlang has no null — the closest equivalent is the atom undefined, % which is just an ordinary atom with no special language treatment: Value = undefined, io:format("~p~n", [Value]).
actor Main new create(env: Env) => // Pony has no null and no exceptions — "absence" is expressed with // a union type instead, e.g. (I64 | None): let value: (I64 | None) = None match value | None => env.out.print("no value") | let n: I64 => env.out.print(n.string()) end
Erlang has no null concept whatsoever — undefined is just a regular atom, with zero special compiler treatment. Pony also has no null and no exceptions in the traditional sense — "a value or nothing" is expressed with a union type like (I64 | None), and the compiler forces you to match both cases before using the value, catching what would be a null-pointer-style bug in most other languages at compile time.
`let` bindings, like Erlang, cannot be reassigned
X = 40, % X = 41 would be a {badmatch,41} error — Erlang variables bind once: io:format("~p~n", [X]).
actor Main new create(env: Env) => let x: I64 = 40 // x = 41 below would be a COMPILE ERROR — "let" bindings in Pony // cannot be reassigned; "var" would be needed for that instead: env.out.print(x.string())
This is a close match: Erlang's X = 40 can never be rebound, full stop. Pony's let x: I64 = 40 gives the identical guarantee — reassignment is a compile error — but, like Rust and F#, Pony offers an explicit escape hatch: declaring the binding var instead of let permits real mutation, something no Erlang variable can ever opt into.