Everything Is a List
Hello, World
The parentheses move. In Elixir the function name sits outside the call's parentheses; in a Lisp the opening paren comes first and everything inside is the operator followed by its arguments. That single rule is the entire syntax.
IO.puts("Hello, World!") (println "Hello, World!") There is no operator precedence to learn, no keywords with special parsing, and no ambiguity about where a call begins and ends — which is exactly what makes the macro system in the last section possible. The parentheses buy homoiconicity, and everything else on this page follows from that trade.
Truthiness is identical
Start with the comfort. Both languages made the same unusual choice: only
nil and false are falsy, and everything else — zero, the empty string, the empty collection — is truthy.for value <- [0, "", [], %{}, nil, false] do
IO.puts("#{inspect(value)} -> #{if value, do: "truthy", else: "falsy"}")
end (doseq [value [0 "" [] {} nil false]]
(println (pr-str value) "->" (if value "truthy" "falsy"))) Coming from almost any other language this rule has to be relearned; coming from Elixir it is already yours. This is not a coincidence — José Valim has been explicit that Clojure was a direct influence on Elixir, and the shared truthiness rule, the protocol system, and the pipe operator are three places where that shows through most plainly.
Atoms → keywords
An Elixir atom and a Clojure keyword are the same idea with the colon on the other side of the word. Both are interned, both are cheap to compare, and both are the standard map key.
person = %{name: "Ada", role: :engineer}
IO.inspect(person[:name])
IO.inspect(person.role)
IO.inspect(is_atom(:engineer)) (def person {:name "Ada" :role :engineer})
(println (:name person))
(println (person :role))
(println (keyword? :engineer)) Clojure adds something Elixir does not have: a keyword is itself a function that looks itself up in a map, and a map is a function that looks up a key. So
(:name person) and (person :name) both work, and (map :name people) replaces the anonymous function you would have written. That is a small piece of syntax you will find yourself missing.Binding names
def creates a namespace-level var; let introduces local bindings in a vector of name/value pairs. Neither rebinds — a let shadows rather than mutating, exactly as Elixir's rebinding does.greeting = "hello"
result =
(fn ->
upper = String.upcase(greeting)
upper <> "!"
end).()
IO.puts(result) (def greeting "hello")
(let [upper (clojure.string/upper-case greeting)
result (str upper "!")]
(println result)) The
let vector is read in pairs and each binding can see the ones before it, so it reads like a sequence of assignments while remaining a single expression. Note also that a def at the top of a file is closer to a module attribute than to a variable — idiomatic Clojure keeps very few of them and passes values as arguments instead.Immutable Data, Shared Ancestry
Persistent data structures, both ways
Both languages give you immutable collections with structural sharing, so "updating" a map returns a new one that reuses everything unchanged. The operation names differ; the guarantee does not.
original = %{name: "Ada", year: 1843}
updated = Map.put(original, :year, 1852)
IO.inspect(original)
IO.inspect(updated) (def original {:name "Ada" :year 1843})
(def updated (assoc original :year 1852))
(println original)
(println updated) This is the deepest thing the two languages agree on, and it means the mental model you already have — nothing you hand to another function can change under you — transfers completely. Where they part company is what happens when you genuinely want change over time, which is the State section.
Four literal collections, not two
Elixir has lists and maps as literals, with tuples for fixed-size groups. Clojure has four, and the distinction between a list and a vector matters: a vector indexes in effectively constant time, a list does not.
numbers = [1, 2, 3]
pair = {:ok, 42}
lookup = %{a: 1, b: 2}
unique = MapSet.new([1, 2, 2, 3])
IO.inspect(Enum.at(numbers, 1))
IO.inspect(elem(pair, 1))
IO.inspect(lookup[:a])
IO.inspect(MapSet.member?(unique, 2)) (def numbers [1 2 3]) ; vector — indexed access
(def items '(1 2 3)) ; list — sequential access
(def lookup {:a 1 :b 2}) ; map
(def unique #{1 2 3}) ; set
(println (nth numbers 1))
(println (first items))
(println (:a lookup))
(println (contains? unique 2)) The quote on
'(1 2 3) is doing real work: an unquoted list is code, so a literal list of data has to be marked as not-to-be-evaluated. In practice you will write vectors almost everywhere, which is why function parameters, let bindings, and literal sequences all use square brackets.Nested updates
Both languages ship a path-based updater for reaching into nested structures. Clojure's pair is
get-in/update-in, and the path is an ordinary vector rather than a special access syntax.state = %{user: %{name: "Ada", visits: 3}}
IO.inspect(get_in(state, [:user, :name]))
IO.inspect(update_in(state, [:user, :visits], &(&1 + 1))) (def state {:user {:name "Ada" :visits 3}})
(println (get-in state [:user :name]))
(println (update-in state [:user :visits] inc)) The Clojure version reads more smoothly for a reason worth noticing:
inc is passed directly where Elixir needs the capture syntax &(&1 + 1), because Clojure's standard library is built from small named functions designed to be passed around. Reaching for a named function instead of a lambda is a habit worth acquiring early.Functions & the Two Pipes
def → defn
A function definition takes a name, an optional docstring, a parameter vector, and a body whose last expression is the return value. There is no
do/end and no explicit return.defmodule Greeter do
@doc "Builds a greeting."
def greet(name), do: "Hello, #{name}!"
end
IO.puts(Greeter.greet("Ada")) (defn greet
"Builds a greeting."
[name]
(str "Hello, " name "!"))
(println (greet "Ada")) The docstring sits in the same position it does in Elixir, and
(doc greet) retrieves it at the REPL. What is missing is interpolation — str concatenates and there is no #{}, which is one of the few places Elixir's syntax is plainly more convenient.Multiple arities in one form
Clojure dispatches on arity, like Elixir, but all the arities live inside a single
defn as separate parameter-vector/body pairs. Note what it does not dispatch on — the values of the arguments.defmodule Joining do
def join(parts), do: join(parts, ", ")
def join(parts, separator), do: Enum.join(parts, separator)
end
IO.puts(Joining.join(["a", "b", "c"]))
IO.puts(Joining.join(["a", "b", "c"], " | ")) (defn join
([parts] (join parts ", "))
([parts separator] (clojure.string/join separator parts)))
(println (join ["a" "b" "c"]))
(println (join ["a" "b" "c"] " | ")) Arity is the only thing a
defn dispatches on. In Elixir a function head can match on the shape and value of its arguments — def handle({:ok, value}) versus def handle({:error, reason}) — and there is no defn equivalent. That gap is the subject of the next section.One pipe → two threading macros
Elixir has one pipe because its standard library consistently takes the data first. Clojure's does not: sequence functions take the collection last while map functions take it first, so there are two threading macros for the two conventions.
result =
[1, 2, 3, 4, 5]
|> Enum.filter(fn number -> rem(number, 2) == 1 end)
|> Enum.map(fn number -> number * 10 end)
|> Enum.sum()
IO.puts(result) ; ->> threads the value into the LAST position (sequence functions)
(println (->> [1 2 3 4 5]
(filter odd?)
(map #(* 10 %))
(reduce +)))
; -> threads into the FIRST position (maps, strings, host interop)
(println (-> {:name "Ada"}
(assoc :role :engineer)
(get :role))) Picking the wrong arrow is the most common beginner mistake, and the error it produces is rarely helpful. The rule that covers almost every case:
->> for anything that operates on a sequence, -> for anything that operates on a map, a string, or a host object. Elixir avoided the whole problem by fixing the argument order library-wide.Anonymous functions and capture syntax
Clojure has the same two forms Elixir does: a full lambda and a terse reader shorthand. In the shorthand,
% is the first argument and %1/%2 are positional — the direct counterpart of &1 and &2.double = fn number -> number * 2 end
IO.inspect(Enum.map([1, 2, 3], double))
IO.inspect(Enum.map([1, 2, 3], &(&1 * 2)))
IO.inspect(Enum.zip_with([1, 2], [10, 20], &(&1 + &2))) (def double (fn [number] (* number 2)))
(println (map double [1 2 3]))
(println (map #(* % 2) [1 2 3]))
(println (map #(+ %1 %2) [1 2] [10 20])) A closure is called exactly like a named function — no
.() — because Clojure has one namespace for values and functions where Elixir has two. That single difference removes a small daily friction, and it is also why a bare symbol like inc can be passed straight to map without a capture operator.No Pattern Matching
The thing you will miss most
This is the largest single loss on the page, and it is worth meeting head-on. Elixir dispatches on the shape and value of arguments across function heads. Clojure has no equivalent — the idiomatic replacement is a conditional inside one body.
defmodule Handler do
def handle({:ok, value}), do: "succeeded with #{value}"
def handle({:error, reason}), do: "failed: #{reason}"
def handle(:pending), do: "still working"
end
IO.puts(Handler.handle({:ok, 42}))
IO.puts(Handler.handle({:error, "timeout"}))
IO.puts(Handler.handle(:pending)) (defn handle [result]
(cond
(= result :pending) "still working"
(= (first result) :ok) (str "succeeded with " (second result))
(= (first result) :error) (str "failed: " (second result))
:else "unrecognized"))
(println (handle [:ok 42]))
(println (handle [:error "timeout"]))
(println (handle :pending)) There is a library —
core.match — that provides real pattern matching, and it is genuinely good, but it is a dependency rather than a language feature and most Clojure code does not use it. The everyday answer is cond, destructuring, or a multimethod. Expect to feel this on every result-handling function you write.Destructuring binds, it does not match
Clojure destructures in
let and in parameter vectors, and the map form with :keys is genuinely more concise than Elixir's. The crucial difference: destructuring never fails, it fills in nil.%{name: name, role: role} = %{name: "Ada", role: :engineer}
IO.puts("#{name} / #{role}")
[first | rest] = [1, 2, 3]
IO.inspect({first, rest})
# A key that is absent raises MatchError rather than binding nil.
try do
%{missing: _value} = %{name: "Ada"}
rescue
MatchError -> IO.puts("MatchError — the shape did not match")
end (let [{:keys [name role]} {:name "Ada" :role :engineer}]
(println (str name " / " role)))
(let [[first & rest] [1 2 3]]
(println first rest))
; An absent key binds nil. Nothing fails, nothing is reported.
(let [{:keys [missing]} {:name "Ada"}]
(println "missing ->" (pr-str missing))) That last cell is the trap. In Elixir a match against the wrong shape raises immediately, at the point the bad data arrived; in Clojure the binding quietly becomes
nil and the failure surfaces somewhere else entirely, usually as a NullPointerException several frames away. Elixir's "let it crash" depends on crashing early, and this is where that guarantee goes missing.cond and case
Both languages have a
cond that tests conditions in order and a case that compares against constants. Clojure's case compares literals only — it does not evaluate its test forms and cannot bind.value = 42
result =
cond do
value < 0 -> "negative"
value < 100 -> "small"
true -> "large"
end
IO.puts(result)
IO.puts(case :green do
:red -> "stop"
:green -> "go"
_ -> "unknown"
end) (def value 42)
(println (cond
(< value 0) "negative"
(< value 100) "small"
:else "large"))
(println (case :green
:red "stop"
:green "go"
"unknown")) Elixir's catch-all is the literal
true; Clojure's is :else, which is not special at all — it is simply a keyword, and every keyword is truthy, so any keyword would work. The final bare expression in case is its default, and omitting it means an unmatched value throws rather than returning nil.Enum → Sequences (Lazy)
Enum and Stream → one lazy sequence
Clojure's sequence functions are lazy by default, so there is no
Enum/Stream decision to make. (range) with no argument is infinite, and only what is consumed gets produced.result =
1..1_000_000
|> Stream.map(fn number -> number * 2 end)
|> Stream.filter(fn number -> rem(number, 3) == 0 end)
|> Enum.take(5)
IO.inspect(result) (println (->> (range)
(map #(* % 2))
(filter #(zero? (mod % 3)))
(take 5))) Where Elixir makes you choose the module, Clojure makes you remember that the choice was made for you — which cuts both ways. Holding onto the head of a long lazy sequence keeps every element realized in memory, the Clojure equivalent of Haskell's space leak, and it is the one performance hazard laziness introduces that
Enum cannot have.Enum.reduce → reduce
The same fold with the same three parts, though the argument order differs: Clojure takes the function first and the collection last, which is what
->> exists to accommodate.total = Enum.reduce([1, 2, 3, 4], 0, fn number, accumulator -> accumulator + number end)
IO.puts(total)
counts =
Enum.reduce(["a", "b", "a"], %{}, fn word, accumulator ->
Map.update(accumulator, word, 1, &(&1 + 1))
end)
IO.inspect(counts) (println (reduce + 0 [1 2 3 4]))
(println (reduce (fn [accumulator word]
(update accumulator word (fnil inc 0)))
{}
["a" "b" "a"])) fnil is worth stealing conceptually even if you cannot take it home: it wraps a function so a nil argument is replaced by a default, which is how Clojure handles the "key not present yet" case that Elixir covers with Map.update/4's third argument. Small combinators like this are where the standard library shows its age advantage.Grouping and frequencies
These two have direct counterparts with almost identical names, which makes them a good illustration of how much of your
Enum vocabulary carries over unchanged.words = ["apple", "avocado", "banana", "blueberry", "cherry"]
IO.inspect(Enum.group_by(words, &String.first/1))
IO.inspect(Enum.frequencies([1, 2, 2, 3, 3, 3])) (def words ["apple" "avocado" "banana" "blueberry" "cherry"])
(println (group-by first words))
(println (frequencies [1 2 2 3 3 3])) Note
first doing double duty: applied to a string it returns the leading character, because a Clojure string is seqable. That uniformity — one set of sequence functions over strings, vectors, maps, sets and lazy sequences alike — is the payoff for having fewer specialized modules than Elixir does.Transducers — no Elixir equivalent
A transducer is a transformation separated from the thing being transformed. Composing
map and filter without a collection produces a reusable pipeline that can then be applied to a sequence, a channel, or a fold — with no intermediate collections built.# Elixir composes over a specific collection; the pipeline and the data
# cannot be separated. Stream avoids the intermediate lists, but the
# composition is still bound to this one source.
pipeline = fn source ->
source
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(&1 > 4))
end
IO.inspect(Enum.to_list(pipeline.([1, 2, 3, 4])))
IO.inspect(Enum.sum(pipeline.([1, 2, 3, 4]))) ; The transformation is a value, defined with no collection in sight.
(def transformation (comp (map #(* % 2)) (filter #(> % 4))))
(println (into [] transformation [1 2 3 4]))
(println (transduce transformation + [1 2 3 4])) The Elixir version comes close by wrapping the pipeline in a function, but the composition still names a source and still produces a stream. A transducer is genuinely just the reducing step, so the same value drives
into, transduce, and core.async channels without knowing what it is feeding. This is the clearest case on the page of Clojure having an abstraction with no Elixir counterpart.Protocols Came From Here
defprotocol → defprotocol
Elixir's protocols are taken from Clojure's, down to the name and the single-dispatch-on-first-argument semantics. The declarations line up almost exactly. (Here the implementation is written inline in the
defrecord, which is what the browser runtime supports.)defprotocol Describable do
def describe(value)
end
defmodule Circle do
defstruct radius: 0
end
defimpl Describable, for: Circle do
def describe(circle), do: "circle of radius #{circle.radius}"
end
IO.puts(Describable.describe(%Circle{radius: 2})) (defprotocol Describable
(describe [this]))
(defrecord Circle [radius]
Describable
(describe [this] (str "circle of radius " (:radius this))))
(println (describe (->Circle 2))) The correspondence is close enough that the Elixir documentation cites Clojure directly. Two differences to keep in mind: Clojure dispatches on the host type where Elixir dispatches on the term's tag, and Clojure has no consolidation step — Elixir compiles protocol dispatch into a lookup table at build time, which is why
Protocol.consolidate exists and has no counterpart here. (The Elixir cell is display-only on this page for an unrelated, purely practical reason: AtomVM compiles a protocol with an implementation too slowly to run in the browser.)Multimethods dispatch on anything
This is what Clojure has that Elixir does not. A protocol dispatches on the type of the first argument; a multimethod dispatches on the result of an arbitrary function of all the arguments — so you can dispatch on a map key, a computed value, or a pair of types.
# Elixir gets this with pattern matching in function heads, which is
# powerful but fixed at compile time and closed to other modules.
defmodule Area do
def area(%{shape: :square, side: side}), do: side * side
def area(%{shape: :rectangle, width: width, height: height}), do: width * height
end
IO.puts(Area.area(%{shape: :square, side: 4}))
IO.puts(Area.area(%{shape: :rectangle, width: 3, height: 5})) (defmulti area :shape)
(defmethod area :square [shape]
(* (:side shape) (:side shape)))
(defmethod area :rectangle [shape]
(* (:width shape) (:height shape)))
(println (area {:shape :square :side 4}))
(println (area {:shape :rectangle :width 3 :height 5})) The dispatch function is
:shape — an ordinary keyword lookup — and any namespace can add a defmethod later without touching the original. That openness is the point: Elixir's pattern-matched heads are closed to the module that declared them, while a multimethod is extensible by code that did not exist when it was written. Multimethods also honor a hierarchy, so :square can derive from :rectangle.defstruct → Maps & Records
Plain maps are the default
Elixir reaches for a struct to give a map a name and a known set of fields. Idiomatic Clojure usually does not — a plain map is the default, and namespaced keywords carry the meaning a struct name would.
defmodule Account do
defstruct id: nil, balance: 0
end
account = %Account{id: 1, balance: 100}
IO.inspect(account)
IO.inspect(account.__struct__) (def account {:account/id 1 :account/balance 100})
(println account)
(println (:account/balance account))
(println (namespace :account/id)) A namespaced keyword like
:account/id says where the key came from without wrapping the map in a type, which keeps every generic map function applicable. The cost is that nothing enforces the shape — no %Account{} to match on, no error for a misspelled key — and the community answer to that is clojure.spec, a runtime validation library rather than a type.defstruct → defrecord
When you do want a named type with fixed fields,
defrecord is the closest match to defstruct. It generates a constructor, behaves as a map, and can implement protocols inline.defmodule Point do
defstruct horizontal: 0, vertical: 0
end
point = %Point{horizontal: 3, vertical: 4}
IO.inspect(point)
IO.inspect(point.horizontal)
IO.inspect(%{point | vertical: 5}) (defrecord Point [horizontal vertical])
(def point (->Point 3 4))
(println point)
(println (:horizontal point))
(println (assoc point :vertical 5)) A record is a map, so
assoc, get and destructuring all work on it — but assoc-ing a key that is not a declared field is allowed and quietly produces a plain map for that key. Coming from structs, where an unknown key is a compile-time error, that permissiveness is the thing to watch.State: Atoms, Not Processes
A process holding state → an atom
Here is the central inversion. On the BEAM, changing state means a process that owns it and messages that ask it to change. In Clojure an atom is a mutable reference to an immutable value, updated by applying a function to it.
defmodule Tally do
use GenServer
def init(count), do: {:ok, count}
def handle_call(:increment, _from, count), do: {:reply, count + 1, count + 1}
end
{:ok, pid} = GenServer.start_link(Tally, 0)
GenServer.call(pid, :increment)
IO.puts(GenServer.call(pid, :increment)) (def counter (atom 0))
(swap! counter inc)
(swap! counter inc)
(println @counter) Notice how much smaller the Clojure version is, and what was lost along with the ceremony. The GenServer had an identity, a mailbox, a supervisor, and the ability to live on another machine; the atom has none of those. What it does have is
compare-and-set semantics — swap! retries its function if another thread got there first — so it is safe under concurrency without a lock.Watching state change
add-watch registers a callback fired on every change, receiving the old and new values. The nearest Elixir equivalent is a process that other processes subscribe to, which is considerably more machinery.defmodule Watched do
use GenServer
def init(state), do: {:ok, state}
def handle_call({:set, value}, _from, previous) do
IO.puts("changed from #{previous} to #{value}")
{:reply, value, value}
end
end
{:ok, pid} = GenServer.start_link(Watched, 0)
GenServer.call(pid, {:set, 1})
GenServer.call(pid, {:set, 2}) (def temperature (atom 20))
(add-watch temperature :logger
(fn [_key _reference previous current]
(println "changed from" previous "to" current)))
(reset! temperature 21)
(swap! temperature inc) The watch runs synchronously on the thread that made the change, which makes it a poor fit for anything slow — there is no mailbox to absorb the work the way a subscribed process would. It is a notification hook, not a supervision or pub/sub mechanism, and reading it as the latter is the way to get into trouble.
Identity, state, and time
Clojure draws a distinction Elixir makes structurally: an identity (something that persists through change) is separate from its state (an immutable value at a moment). Dereferencing an atom gives you a value that will never change, even as the atom moves on.
defmodule Snapshot do
use GenServer
def init(state), do: {:ok, state}
def handle_call(:get, _from, state), do: {:reply, state, state}
def handle_call({:put, key, value}, _from, state) do
{:reply, :ok, Map.put(state, key, value)}
end
end
{:ok, pid} = GenServer.start_link(Snapshot, %{count: 1})
snapshot = GenServer.call(pid, :get)
GenServer.call(pid, {:put, :count, 2})
IO.inspect(snapshot)
IO.inspect(GenServer.call(pid, :get)) (def registry (atom {:count 1}))
(def snapshot @registry)
(swap! registry assoc :count 2)
(println snapshot)
(println @registry) You already get this for free, and may never have named it: a term received in a message is a copy that nothing can mutate, so it is a snapshot by construction. Clojure achieves the same guarantee without copying, because the value is immutable and can therefore be shared safely — one deref, and what you hold is stable no matter what happens to the atom next.
Shared Memory & STM
Threads share one heap
The model inverts completely. BEAM processes share nothing and communicate by copying; Clojure threads share one heap and communicate through references whose update semantics are controlled. This cell cannot run here — the browser runtime is ClojureScript, which is single-threaded and has no
future.parent = self()
for number <- 1..3 do
spawn(fn -> send(parent, {:done, number * number}) end)
end
results =
for _ <- 1..3 do
receive do
{:done, value} -> value
end
end
IO.inspect(Enum.sort(results)) ; Each future runs on a real thread from a shared pool.
(def computations
(doall (for [number [1 2 3]]
(future (* number number)))))
; Dereferencing a future blocks until it has a value.
(println (sort (map deref computations))) A
future has no identity you can message, no mailbox, no link, and no monitor — dereferencing it is the only interaction, and an exception inside it is re-thrown at the point of deref. There is no supervision layer anywhere in Clojure: the JVM gives you thread pools and the language gives you reference types, and the restart strategy is whatever you build.Coordinated change → refs and dosync
This is the piece with no BEAM counterpart. Software transactional memory lets several independent references change together atomically, with the runtime retrying on conflict. Display-only here: ClojureScript has no STM.
# Two balances can only change atomically if ONE process owns both.
defmodule Vault do
use GenServer
def init(state), do: {:ok, state}
def handle_call({:transfer, amount}, _from, {source, destination}) do
moved = {source - amount, destination + amount}
{:reply, moved, moved}
end
end
{:ok, pid} = GenServer.start_link(Vault, {100, 0})
IO.inspect(GenServer.call(pid, {:transfer, 30})) ; Two refs that knew nothing about each other until this transaction.
(def source (ref 100))
(def destination (ref 0))
(dosync
(alter source - 30)
(alter destination + 30))
(println [@source @destination]) The Elixir version works only because both balances were deliberately placed inside one process. Once they live in two processes, no primitive moves value between them atomically — you write a coordinator or a two-phase protocol by hand. Here the two refs were unrelated until
dosync said otherwise, and the runtime handles retry and isolation.Agents — the same word, a different thing
Clojure has an
agent and so does Elixir, and they are not the same idea. An Elixir Agent is a process wrapping state that you query synchronously. A Clojure agent is a reference updated asynchronously by a thread pool. Display-only: no agents in ClojureScript.# Elixir's Agent: a process. You send it a function and WAIT for the reply.
{:ok, pid} = Agent.start_link(fn -> 0 end)
Agent.update(pid, fn count -> count + 1 end)
Agent.update(pid, fn count -> count + 1 end)
IO.puts(Agent.get(pid, fn count -> count end)) ; Clojure's agent: send returns IMMEDIATELY; the update happens later
; on a pooled thread. await blocks until queued actions have run.
(def total (agent 0))
(send total + 1)
(send total + 1)
(await total)
(println @total) The overlap in naming is unfortunate and worth holding onto, because the failure modes differ. An Elixir
Agent.update is a synchronous call that will time out if the process is busy; a Clojure send always returns immediately, so backpressure is invisible and errors surface later through agent-error rather than at the call site.Mailboxes → core.async channels
The nearest thing to message passing is
core.async, a library rather than a language feature, providing CSP-style channels and a go block. Display-only: the library is not bundled with the browser runtime.send(self(), {:low, "low priority"})
send(self(), {:high, "high priority"})
# Selective receive: take the high-priority message even though the
# low one arrived first, and leave the other in the mailbox.
receive do
{:high, message} -> IO.puts(message)
end
receive do
{:low, message} -> IO.puts(message)
end (require '[clojure.core.async :as async])
(def high (async/chan 4))
(def low (async/chan 4))
(async/>!! low "low priority")
(async/>!! high "high priority")
; alts!! chooses between CHANNELS. Within one channel it is strictly
; FIFO — there is no way to look past the front and take a later message.
(println (first (async/alts!! [high low])))
(println (first (async/alts!! [high low]))) Two things are missing relative to a mailbox, and both matter. There is no selective receive —
alts!! picks between channels, not between messages inside one — so a process that defers work it is not ready for has no direct translation. And a channel is a value you create and hand around, where a pid is a universal address any holder can write to, which is what makes OTP's registry and supervision possible.Errors on a Host That Throws
Tagged tuples → exceptions
Elixir reserves exceptions for the exceptional and returns
{:ok, _}/{:error, _} for everything expected. Clojure sits on hosts where throwing is the convention, so exceptions are the ordinary mechanism.defmodule Configuration do
def fetch_required(config, key) do
case Map.fetch(config, key) do
{:ok, value} -> {:ok, value}
:error -> {:error, {:missing, key}}
end
end
end
config = %{host: "localhost"}
IO.inspect(Configuration.fetch_required(config, :host))
IO.inspect(Configuration.fetch_required(config, :port)) (defn fetch-required [config key]
(if-let [value (get config key)]
value
(throw (ex-info "missing configuration" {:key key}))))
(def config {:host "localhost"})
(println (fetch-required config :host))
(println (try
(fetch-required config :port)
(catch :default error
[:error (ex-message error) (ex-data error)]))) ex-info is the idiomatic way to throw with structured data attached, and ex-data retrieves the map — which is as close as Clojure comes to Elixir's tagged error tuples. The habit of returning errors as values does exist in the community, but it is a style choice per codebase rather than the language-wide convention it is in Elixir.nil punning cuts both ways
Both languages have
nil and both treat it as falsy. Clojure goes further: most core functions accept nil and do something sensible with it rather than failing, which is convenient right up until it is not.IO.inspect(Enum.count([]))
IO.inspect(Map.get(%{}, :missing))
# Passing nil where a collection is expected raises immediately.
try do
Enum.count(nil)
rescue
Protocol.UndefinedError -> IO.puts("Protocol.UndefinedError — raised at the call")
end (println (count []))
(println (get {} :missing))
; nil is treated as an empty collection. No error, no warning.
(println (count nil))
(println (map inc nil))
(println (first nil)) This is the single largest cultural difference in error handling between the two languages. Elixir's "let it crash" is only useful because things crash early, close to the bad data; nil punning deliberately postpones the crash, so a
nil from a typo in a keyword flows through several transformations before anything objects. Combined with destructuring that never fails, it is the main reason Clojure stack traces point somewhere other than the bug.Guards → pre/post conditions
Clojure has no guard clauses, but a function can carry a map of
:pre and :post assertions that throw when violated. It is a runtime check rather than a dispatch mechanism.defmodule Withdrawal do
def withdraw(balance, amount) when amount > 0 and amount <= balance do
balance - amount
end
end
IO.puts(Withdrawal.withdraw(100, 30))
try do
Withdrawal.withdraw(100, 500)
rescue
FunctionClauseError -> IO.puts("FunctionClauseError — no clause matched")
end (defn withdraw [balance amount]
{:pre [(pos? amount) (<= amount balance)]}
(- balance amount))
(println (withdraw 100 30))
; NOTE the catch target: a failed :pre throws an AssertionError, which on
; the JVM is an Error rather than an Exception, so the broad catch is needed.
(println (try
(withdraw 100 500)
(catch js/Error error "assertion failed — no clause to fall through to"))) The difference is what happens on failure. An Elixir guard that does not hold means this clause does not apply, so another clause may still match — guards are part of dispatch. A failed
:pre throws outright; there is nothing to fall through to. Preconditions can also be compiled out entirely, which makes them a debugging aid rather than a contract you can rely on in production.Strings & Keywords
String → clojure.string
The function names line up closely, in a namespace conventionally aliased as
str or string. The argument order is the Clojure sequence convention, so these thread with -> rather than ->>.line = " one,two,three "
trimmed = String.trim(line)
parts = String.split(trimmed, ",")
IO.inspect(parts)
IO.puts(Enum.join(parts, " | "))
IO.puts(String.upcase(trimmed)) (def line " one,two,three ")
(let [trimmed (clojure.string/trim line)
parts (clojure.string/split trimmed #",")]
(println parts)
(println (clojure.string/join " | " parts))
(println (clojure.string/upper-case trimmed))) The
#"," is a regular-expression literal — split takes a pattern, not a plain string, which catches people out because a literal string argument fails rather than splitting on itself. Elixir accepts either a binary or a regex, and this is one of the few places its API is the more forgiving of the two.Interpolation → str
There is no
#{}. Building a string means str, which concatenates anything after converting it, or format for a template with placeholders.name = "Ada"
year = 1843
IO.puts("#{name} wrote the first algorithm in #{year}")
IO.puts(String.pad_leading("42", 5, "0")) (def name "Ada")
(def year 1843)
(println (str name " wrote the first algorithm in " year))
; format is JVM-only, so padding is written out by hand to run in both
; dialects — a small taste of the .cljc problem the Host section covers.
(let [text (str 42)]
(println (str (apply str (repeat (- 5 (count text)) "0")) text))) The absence is felt most in log lines and error messages, where Elixir's interpolation is genuinely more readable.
str does have one advantage worth noting: it handles nil as an empty string rather than failing, so a missing value degrades to a gap in the output instead of raising — which is nil punning again, helpful and hazardous in the same breath. And note the comment: format exists on the JVM and not in ClojureScript, which is the two-dialect split showing up in something as ordinary as padding a number.Homoiconicity, One Step Further
AST tuples → the code itself
Elixir is homoiconic in a qualified sense:
quote gives you a tree of {name, metadata, arguments} tuples, which is a faithful representation of the code but is not what you typed. In a Lisp there is no representation step — the list you typed is the data structure.quoted = quote do: 1 + 2 * 3
IO.inspect(quoted)
IO.inspect(elem(quoted, 0))
IO.inspect(Code.eval_quoted(quoted) |> elem(0)) (def quoted '(+ 1 (* 2 3)))
(println quoted)
(println (first quoted))
(println (count quoted))
(println (eval quoted)) That difference is why Clojure code manipulating code reads like code manipulating lists — because it is. Elixir's tuples are a very good approximation and support the same techniques, but you are always working with a description of the syntax rather than the syntax, and the infix operators that make Elixir pleasant to read are exactly what force that indirection.
defmacro → defmacro
Both take unevaluated arguments and return code. The syntax rhymes: Elixir's
quote/unquote pair is Clojure's backtick and ~, and the splicing forms unquote_splicing and ~@ match too. 🚨 The Clojure cell is display-only for a runtime reason, not a language one — see the note below.defmodule Unless do
defmacro unless_true(condition, do: body) do
quote do
if unquote(condition), do: nil, else: unquote(body)
end
end
end
require Unless
Unless.unless_true(false) do
IO.puts("the body ran")
end (defmacro unless-true [condition body]
`(if ~condition nil ~body))
(println (unless-true false "the body ran"))
; Expansion, which is what a macro is for:
(println (macroexpand-1 '(unless-true false "the body ran"))) 🚨 This cell cannot run in the browser, and the reason is worth knowing if you write Clojure in any hosted evaluator: the page's runtime (SCI) analyzes a whole snippet as one unit, so a macro defined and used in the same snippet is compiled as a function call before it is known to be a macro. It then prints
(if nil nil nil) — a plausible wrong answer, with no error. Split across two evaluations it behaves correctly. Elixir has no equivalent hazard because require makes the macro available as its own compilation step.Hygiene: automatic vs. requested
Elixir macros are hygienic by default — a variable introduced in a
quote cannot capture one at the call site unless you ask for it with var!. Clojure inverts the default: you request a fresh name with a trailing #.defmodule Doubling do
defmacro with_doubled(value, do: body) do
quote do
doubled = unquote(value) * 2
unquote(body)
end
end
end
require Doubling
doubled = "the caller's own binding"
Doubling.with_doubled(21) do
IO.puts(doubled)
end ; The trailing # generates a guaranteed-unique symbol, so the macro
; cannot capture a binding of the same name at the call site.
(defmacro with-doubled [value body-fn]
`(let [result# (* ~value 2)]
(~body-fn result#)))
(println (macroexpand-1 '(with-doubled 21 println))) The Elixir cell demonstrates hygiene working: the macro binds
doubled internally, and the IO.puts(doubled) at the call site still sees the caller's own binding, printing the string rather than 42. Clojure would capture in that situation unless the author wrote result#. Neither default is obviously right — Elixir's is safer, Clojure's makes deliberate anaphoric macros easy — but knowing which way it falls prevents a confusing class of bug.The Host Underneath
A host platform under the language
The BEAM was built for Erlang, so Elixir has no foreign platform to reach into — its interop story is calling Erlang, a peer language on the same VM. Clojure is a hosted language, and reaching the host is routine and syntactic.
# "Interop" means calling Erlang — same VM, same terms, no conversion.
IO.inspect(:math.pow(2, 10))
IO.inspect(:erlang.system_info(:otp_release))
IO.inspect(:lists.reverse([1, 2, 3])) ; Host interop has its own syntax: Class/staticMethod and .instanceMethod.
(println (Math/pow 2 10))
(println (.toUpperCase "hello"))
(println (.indexOf "hello world" "world")) The trade is visible in both directions. Clojure gets an enormous existing ecosystem — every JVM or npm library is available — at the cost of host types leaking into your code, mutable objects, null, and platform-specific behavior that differs between Clojure and ClojureScript. Elixir gets a single platform with one consistent data model, and whatever is not on the BEAM is not available at all.
Two dialects, one language
Clojure targets the JVM and ClojureScript targets JavaScript, and they are not the same language — this page is proof, since its browser runtime is ClojureScript and several examples above are display-only because of it.
# One language, one runtime. Code that compiles, runs — everywhere the
# BEAM runs, with the same semantics.
IO.inspect(Enum.map([1, 2, 3], &(&1 * 2)))
IO.inspect(:erlang.system_info(:otp_release)) ; A .cljc file compiles for BOTH, with reader conditionals for the
; places they differ. This is everyday practice, not an edge case.
(println (map #(* % 2) [1 2 3]))
; #?(:clj (println "on the JVM: refs, agents, futures, real threads")
; :cljs (println "in the browser: none of those exist")) The differences are not cosmetic: STM, agents, futures, real threads and
Integer semantics are JVM-only, while ClojureScript gets JavaScript's single number type and single thread. Reader conditionals in .cljc files paper over the gaps, and a large amount of shared library code is written that way. Coming from a language with exactly one runtime, this is the structural complexity to budget for.Mix → deps.edn
mix → the CLI and deps.edn
Elixir settled on Mix early and universally. Clojure took longer and has two common answers — the official
clojure CLI with deps.edn, and Leiningen, which predates it. These are shell commands, so neither cell runs here.mix new my_app
mix deps.get
mix test
mix format
iex -S mix clojure -Ttools install-latest :as new
clojure -Tnew app :name my/app
clojure -M:test
cljfmt fix
clojure -M:repl/rebel The REPL is where the cultures diverge most.
iex -S mix is a useful tool you reach for occasionally; the Clojure REPL is the primary development interface, connected to a running application from the editor, with code evaluated form by form as you write it. Adopting that workflow is a bigger adjustment than any syntax on this page, and it is the thing Clojure programmers will tell you they cannot work without.mix.exs → deps.edn
Both declare dependencies in a manifest at the project root.
deps.edn is EDN — Clojure's data notation — so it is literally a map, read by the same reader that reads code.# mix.exs
defp deps do
[
{:jason, "~> 1.4"},
{:ecto, "~> 3.12"}
]
end ;; deps.edn
{:deps {cheshire/cheshire {:mvn/version "5.13.0"}
com.github.seancorfield/next.jdbc {:mvn/version "1.3.939"}}
:aliases {:test {:extra-paths ["test"]}}} Two differences worth knowing. Versions are exact rather than ranges — there is no
~>, and resolution picks the newest version any dependency asks for, which is simpler and occasionally surprising. And deps.edn can depend directly on a git SHA with no published artifact at all, which Hex has no equivalent for.