PONY λ M2 Modula-2
for Elixir programmers

You already know Elixir.Now explore other languages.

Side-by-side, interactive cheatsheets for Elixir programmers
comparing Elixir to other languages. Every example runs live in your browser — no setup, no installation.

▶ Start with Ruby Browse comparisons ↓

Choose your own path by reordering languages

Ruby ⚡ Works Offline ⚡ Offline

The language Elixir borrowed its clothes from — travel José Valim's road in reverse. The #{} interpolation, %w sigils, do/end blocks, and only-nil-and-false-falsy rule all feel like home; what changes underneath is everything: behavior lives ON the data, mutation and aliasing are normal, and exceptions replace tagged tuples.

  • Behavior moves onto data: String.upcase(text) becomes text.upcase — objects carry their methods, and polymorphic dispatch replaces pattern-match function heads
  • Mutation and ALIASING are real: two names can watch each other's edits (second << 4 changes first) — the bug class the BEAM structurally deleted returns, with dup/freeze/Data.define as the opt-outs
  • = is just assignment — Ruby's real pattern matching lives in case/in (Ruby 3+), with hash/array patterns, guards, and match-or-raise semantics
  • Blocks, not fn arguments, are the everyday closure — do |x| … end attached outside the argument list, invoked with yield; &:symbol plays the capture operator
  • No pipe operator: methods returning receivers make chaining THE pipeline, with .then for the leftover one-step case
  • Mixins are the superpower running the other way: include Comparable + one <=> buys every comparison operator — protocols dispatch but never donate implementations like this
  • The concurrency downgrade, stated honestly: shared-memory threads under a GVL, no isolation, no supervision — with Ractors as Ruby's experimental, BEAM-inspired counter-move
Go Pre-Alpha

Cheap concurrency without the isolation — and that changes everything. A goroutine costs about what a process costs, but it shares one heap, one garbage collector, and every variable it can reach: data races are back, locks are back, and an unrecovered panic in any goroutine kills the whole program. What you get in return is a compiler that rejects badarith before the binary exists, and a deploy that is one static file.

  • go func() hands back nothing — no pid, so no send, no link, no monitor, no Process.exit; coordination must be arranged in advance through a channel you both hold
  • A channel is a typed FIFO queue with no selective receive — you cannot pluck the urgent message past an earlier one, and rebuilding priority means several channels plus a select you write yourself
  • "Let it crash" has no floor: one unrecovered panic takes down every goroutine at once, so there is nothing to supervise and no supervision tree — context propagates cancellation and nothing restarts
  • Pattern matching is simply gone — = only assigns, function heads carry no patterns, guards become a switch, and the type switch is the one place Go binds a variable while branching on shape
  • {:ok, value} / {:error, reason} splits into two return values and with becomes the if err != nil ladder, written out at every step
  • The pleasant surprise: interfaces are satisfied structurally, with no defimpl and no @behaviour to name — closer to protocols than a declared contract ever was
Kotlin Pre-Alpha

The other modern language your team might have picked — and a concurrency INVERSION. Coroutines are cheap like processes but share one heap (locks return), scheduling is cooperative rather than preemptive, and structured concurrency cancels where supervision restarts. In exchange: static types with inference, null safety with teeth, and the JVM.

  • The concurrency inversion, honestly told: shared mutable state needs a Mutex nothing forces you to use; a tight loop can hog a dispatcher thread (yield() by hand); channels are FIFO with no selective receive
  • supervisorScope borrows OTP's word but only half its meaning: it contains failure — nothing RESTARTS; there is no known-good init state to return to
  • Null safety stronger than nil ever was: Int? is a distinct type, no truthiness anywhere, ?./?: as the safe plumbing
  • OOP returns: behavior moves onto data (rectangle.area()), defstructdata class with copy() as the update syntax, and == stays structural — a rhyme
  • when is NOT a pattern match: no shape destructuring in branches — sealed hierarchies + smart casts + exhaustive when are the compiler-checked recovery
  • Read-only collections are the default (listOf has no mutating members, list + element builds anew) — the Elixir instinct works, though it is an interface, not deep immutability
  • No pipe: methods-on-receivers make chaining the pipeline, with .let { } as the one-step then/2
Rust Pre-Alpha

The language you reach for when the BEAM cannot go fast enough — and the one place its isolation guarantee does not reach. Pattern matching, immutability by default, and tagged results all come with you; match is case with exhaustiveness checking and traits are very nearly protocols. What is new is ownership, and what inverts is concurrency: threads share one heap, locks are back, a channel has no selective receive, and panic! has no supervisor beneath it.

  • Rustler is why most Elixir developers get here — and a NIF runs inside the VM, on the scheduler's own thread, so a panic there takes down the whole node rather than one process
  • Ownership is the one genuinely new idea: each value has a single owner, assignment moves it, and & lends it — which is how memory is freed deterministically with no garbage collector and no GC pause in your latency budget
  • {:ok, value} / {:error, reason} becomes the built-in Result<T, E>, with becomes the ? operator, and nil becomes Option<T> — a different type from the value, so the compiler makes you handle the missing case
  • casematch transfers almost unchanged, now checked for exhaustiveness — but there is no pin operator, so a bare name in a pattern always binds and comparing against an existing variable needs a guard
  • defprotocoltrait is the closest correspondence on the page, until coherence (the orphan rule) refuses what defimpl allows freely
  • Concurrency inverts: an OS thread costs megabytes where a process costs words, a channel is FIFO with no selective receive, shared state means Arc<Mutex<T>>, and nothing restarts a thread that died
Clojure Pre-Alpha ⚡ Works Offline ⚡ Offline

The language Elixir borrowed from, met directly. Protocols are named after and taken from Clojure's, |> is ->, atoms are keywords, and truthiness is identical — so a surprising amount is already yours. Then the divergences land: there is no pattern matching, state is shared memory guarded by references rather than owned by processes, and a host platform with null and exceptions sits underneath everything.

  • No pattern matching — the thing you will miss most. defn dispatches on arity only, destructuring binds nil instead of failing, and the everyday replacement is cond. Real matching is core.match, a library most code does not use
  • Shared ancestry you can feel: immutable persistent collections, only nil and false falsy, keywords as interned map keys, and protocols with single dispatch on the first argument
  • One pipe becomes two — -> threads into the first position and ->> into the last, because the standard library did not fix a single argument order the way Elixir's did
  • Multimethods and transducers have no Elixir counterpart: dispatch on any function of all the arguments, extensible from another namespace, and a transformation that is a value with no collection attached
  • State inverts: an atom is a mutable reference to an immutable value with compare-and-set retry, where you would have started a process — no identity, no mailbox, no supervisor, and no distribution
  • Homoiconicity goes one step further than quote: Elixir gives you a tree of {name, meta, args} tuples describing the code, while a Lisp's list is the code
Haskell Pre-Alpha

Where half of Elixir's good ideas came from, taken all the way. Pattern matching, guards, comprehensions and pipelines are all here and barely changed — but Stream is the default rather than a choice, purity is enforced by the type system rather than by discipline, and with turns out to have been a hand-rolled version of something general. It is also the one target whose concurrency story is a peer rather than a downgrade.

  • with IS do notation — and the same block works over Maybe, Either and IO alike, each failure carrying its own reason instead of collapsing into one anonymous else
  • Laziness by default: your Stream is simply how expressions evaluate, which buys infinite lists like fibonacci = 0 : 1 : zipWith (+) fibonacci (tail fibonacci) and costs you thunks, foldl space leaks, and a performance model to learn
  • Effects live in the type: String -> String is a proof the function cannot print, mutate, or open a socket — the distinction Elixir cannot make between build/1 and announce/1
  • Protocols become type classes, which dispatch on the return type as well: read "42" :: Int and minBound :: Char have no Elixir equivalent at any price
  • GHC's runtime is the closest cousin to the BEAM in mainstream use — millions of green threads, and killThread really does address one of them — but they share one heap, so MVars and deadlocks come back and nothing restarts a thread that died
  • STM is the thing the BEAM does not have: composable atomic transactions across independent variables, with retry for blocking, where coordinating two GenServers atomically means writing the protocol yourself
Pony Pre-Alpha

Actors you already understand, wrapped around one idea you have never had. actor is a process, be is GenServer.cast, and each one owns a private, independently collected heap — so half of Pony costs you nothing to learn. The other half is reference capabilities: where the BEAM buys isolation by copying every single message, Pony proves at compile time that sharing is safe, and hands a megabyte between actors without copying a byte.

  • Six reference capabilities (iso, val, ref, box, tag, trn) replace copy-on-send — and a pid turns out to have been a tag all along
  • Full static typing, with no nil anywhere: "might be missing" is (String | None) in the signature, and the compiler will not let you skip the check
  • Backpressure is built into the runtime — an overloaded actor mutes its senders, so the unbounded-mailbox failure mode needs no GenStage
  • The honest losses: no supervision trees, no GenServer.call (a Promise and a callback instead), no selective receive, no named processes, and no distribution
  • No macros at all — no defmacro, no use, no apply/3 — so every DSL you rely on becomes plain functions and traits
  • The BEAM preempts you by reductions and Pony never does: a long-running behavior holds its scheduler thread, and no Process.exit can stop it
Erlang Pre-Alpha

The language under your floorboards. Same VM, same processes, same maps, same tagged tuples, same OTP — so this page is mostly a syntax and idiom translation for reading OTP source and dependencies, plus the real gaps: no rebinding, no pipe, no with, and two kinds of string.

  • The Prolog-heritage punctuation decoded: commas separate expressions, semicolons separate clauses, a period ends the form — the content of most examples is otherwise identical line-for-line
  • Variables are Capitalized and SINGLE-ASSIGNMENT — rebinding is a badmatch crash, there is no pin operator because every bound variable is already pinned, and real code chains State1/NewState
  • "hello" is a LIST of integers; <<"hello">> is what you call a string — the trap the ~c sigil exists for
  • listsEnum but the arguments FLIP: fun first, list last — Elixir reversed the order for the pipe, and there is no pipe here
  • No with, no cond (Erlang's if IS cond), no truthiness, no nil — and =:=/=/= are your ===/!==
  • send(pid, msg) is Pid ! Msg, Process.monitor is monitor/2, GenServer IS gen_server — the process model and OTP transfer without translation
  • -define is a C-style textual preprocessor — Elixir's quote/unquote AST macros are the far more powerful descendant
Elm Pre-Alpha

The front end of the BEAM-adjacent functional world. The shared ground is huge — immutability, pattern matching, pipes, expressions everywhere — so what changes is the guarantees: static types with full inference, no nil, no atoms, exhaustive case at compile time, and The Elm Architecture as the one gen_server your whole app becomes.

  • Static types with full Hindley–Milner inference: annotations look like @spec but are enforced by the compiler on every build — badarith and FunctionClauseError move to compile time
  • No nil and no truthiness: Maybe replaces the silent-nil bracket lookup, with Maybe.withDefault as a typed || immune to the false-vs-nil bug
  • No atoms: ad-hoc symbols become declared custom-type constructors — a closed, typo-proof set the compiler checks exhaustively
  • The pipe survives but flips: |> feeds the LAST argument, because every function is curried and partial application is free
  • Patterns transfer, guards do not — there is no when; conditions move into if/else inside the branch
  • Purity is enforced, not conventional: no function can sneak in I/O — effects are Cmd data the runtime executes
  • No processes, no supervision: The Elm Architecture is one gen_server-shaped loop (init/update over messages) — and LiveView is this exact architecture run server-side
Drag cards to reorder · your order is saved locally