Side-by-side, interactive cheatsheets for Elixir programmers
comparing Elixir to other languages. Every example runs live in your browser — no
setup, no installation.
Choose your own path by reordering languages
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.
String.upcase(text) becomes text.upcase — objects carry their methods, and polymorphic dispatch replaces pattern-match function headssecond << 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 semanticsfn arguments, are the everyday closure — do |x| … end attached outside the argument list, invoked with yield; &:symbol plays the capture operator.then for the leftover one-step caseinclude Comparable + one <=> buys every comparison operator — protocols dispatch but never donate implementations like thisCheap 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 holdselect you write yourselfpanic takes down every goroutine at once, so there is nothing to supervise and no supervision tree — context propagates cancellation and nothing restarts= 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 stepdefimpl and no @behaviour to name — closer to protocols than a declared contract ever wasThe 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.
Mutex nothing forces you to use; a tight loop can hog a dispatcher thread (yield() by hand); channels are FIFO with no selective receivesupervisorScope borrows OTP's word but only half its meaning: it contains failure — nothing RESTARTS; there is no known-good init state to return toInt? is a distinct type, no truthiness anywhere, ?./?: as the safe plumbingrectangle.area()), defstruct → data class with copy() as the update syntax, and == stays structural — a rhymewhen is NOT a pattern match: no shape destructuring in branches — sealed hierarchies + smart casts + exhaustive when are the compiler-checked recoverylistOf has no mutating members, list + element builds anew) — the Elixir instinct works, though it is an interface, not deep immutability.let { } as the one-step then/2The 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.
& 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 casecase → match 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 guarddefprotocol → trait is the closest correspondence on the page, until coherence (the orphan rule) refuses what defimpl allows freelyArc<Mutex<T>>, and nothing restarts a thread that diedThe 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.
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 usenil and false falsy, keywords as interned map keys, and protocols with single dispatch on the first argument-> threads into the first position and ->> into the last, because the standard library did not fix a single argument order the way Elixir's didatom 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 distributionquote: Elixir gives you a tree of {name, meta, args} tuples describing the code, while a Lisp's list is the codeWhere 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 elseStream 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 learnString -> String is a proof the function cannot print, mutate, or open a socket — the distinction Elixir cannot make between build/1 and announce/1read "42" :: Int and minBound :: Char have no Elixir equivalent at any pricekillThread really does address one of them — but they share one heap, so MVars and deadlocks come back and nothing restarts a thread that diedretry for blocking, where coordinating two GenServers atomically means writing the protocol yourselfActors 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.
iso, val, ref, box, tag, trn) replace copy-on-send — and a pid turns out to have been a tag all alongnil anywhere: "might be missing" is (String | None) in the signature, and the compiler will not let you skip the checkGenServer.call (a Promise and a callback instead), no selective receive, no named processes, and no distributiondefmacro, no use, no apply/3 — so every DSL you rely on becomes plain functions and traitsProcess.exit can stop itThe 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.
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 forlists ≈ Enum but the arguments FLIP: fun first, list last — Elixir reversed the order for the pipe, and there is no pipe herewith, 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 descendantThe 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.
@spec but are enforced by the compiler on every build — badarith and FunctionClauseError move to compile timeMaybe replaces the silent-nil bracket lookup, with Maybe.withDefault as a typed || immune to the false-vs-nil bug|> feeds the LAST argument, because every function is curried and partial application is freewhen; conditions move into if/else inside the branchCmd data the runtime executes