A language tour
Millions of lightweight processes. Fault-tolerant by design. The Erlang VM wearing a modern face.
01 โ Processes
Elixir processes are not OS threads โ they're lightweight actors managed by the BEAM virtual machine. You can spawn millions of them. They share no memory. They communicate by sending messages. Concurrency becomes simple when state can't be shared.
"Elixir is the language I wish I had when I was building Ruby on Rails. It's pragmatic, productive, and brings the power of the Erlang ecosystem to a modern, friendly syntax."
โ Josรฉ Valim, creator of Elixir# Spawn a lightweight process pid = spawn(fn -> receive do {:greet, name} -> IO.puts("Hello, #{name}!") end end) # Send a message โ async, non-blocking send(pid, {:greet, "Elixir"}) # "Hello, Elixir!" # Spawn 100,000 processes โ this is fast and cheap pids = for i <- 1..100_000 do spawn(fn -> Process.sleep(1000); i * i end) end
Each Elixir process has its own garbage-collected heap. The BEAM scheduler runs them cooperatively across all CPU cores. 100,000 processes is not unusual โ it's the model.
02 โ The Pipe Operator
The pipe operator |> passes the result of one expression as the first argument to the next function. It transforms nested, inside-out code into a linear, readable pipeline โ left to right, top to bottom.
# Without pipe โ nested, inside-out String.upcase(String.trim(String.reverse(" hello "))) # With pipe โ reads like a sentence " hello " |> String.reverse |> String.trim |> String.upcase # "OLLEH" # Data processing pipeline [1, 2, 3, 4, 5, 6] |> Enum.filter(fn x -> rem(x, 2) == 0 end) |> Enum.map(fn x -> x * x end) |> Enum.sum # 56
The pipe operator transforms the mental model from "call functions on data" to "data flowing through transformations." It's the same computation โ the readability difference is significant.
03 โ Pattern Matching
Pattern matching in Elixir isn't just a switch statement โ the = sign itself is a match operator. It binds variables and asserts structure simultaneously. It's how you unpack function arguments, handle errors, and destructure data.
# = is a match operator, not assignment {a, b, c} = {1, 2, 3} # a=1, b=2, c=3 [head | tail] = [1,2,3] # head=1, tail=[2,3] # Function clauses match on arguments defmodule Greeter do def greet({:ok, name}), do: "Hello, #{name}!" def greet({:error, reason}), do: "Error: #{reason}" def greet(:anonymous), do: "Hello, stranger!" end Greeter.greet({:ok, "Josรฉ"}) # "Hello, Josรฉ!" Greeter.greet(:anonymous) # "Hello, stranger!"
Multiple function clauses with different patterns is Elixir's idiomatic way of handling cases. The runtime dispatches to the first clause whose pattern matches โ a cleaner alternative to if/else chains.
04 โ OTP & Supervisors
OTP is a set of libraries and design patterns for building fault-tolerant systems. Supervisors watch processes and restart them when they crash. You don't write defensive code โ you let it crash and rely on the supervision tree to recover.
defmodule Cache do use GenServer # Generic Server behaviour def start_link(_), do: GenServer.start_link(__MODULE__, %{}, name: __MODULE__) def init(state), do: {:ok, state} def put(key, val), do: GenServer.cast(__MODULE__, {:put, key, val}) def get(key), do: GenServer.call(__MODULE__, {:get, key}) def handle_cast({:put, k, v}, state), do: {:noreply, Map.put(state, k, v)} def handle_call({:get, k}, _from, state), do: {:reply, state[k], state} end # Supervisor restarts Cache if it crashes โ automatically
A GenServer is an ordinary Elixir process with a standard message-handling protocol. The OTP framework handles the boilerplate โ timeouts, calls vs. casts, initialisation โ leaving you to describe the state transitions.
05 โ Immutability
All data in Elixir is immutable. You don't modify a map โ you create a new one with the change applied. This makes reasoning about concurrent code trivial: if data can't change, it can't be corrupted by a concurrent process.
# Maps are immutable โ updates return new maps user = %{name: "Alice", age: 30, role: :admin} # Create a new map with updated age older_user = %{user | age: 31} # user is unchanged IO.inspect(user.age) # 30 IO.inspect(older_user.age) # 31 # Rebinding a variable doesn't mutate โ it creates a new binding x = 1 x = 2 # x now points to a new value; the old 1 is unchanged # Deep update with put_in settings = %{db: %{host: "localhost", port: 5432}} put_in(settings, [:db, :port], 5433)
%{user | age: 31} is a structural update โ the BEAM shares unchanged parts of the old map with the new one. Immutability doesn't mean copying everything; it means never mutating shared memory.
06 โ The Whole Picture
Phoenix LiveView builds real-time interactive web apps without JavaScript, using server-side rendering over WebSockets.
Erlang/OTP powered WhatsApp to 2 billion users. Elixir runs on the same VM โ those guarantees transfer.
Deploy new code to a running system without stopping it. A feature from telecom systems, now for web services.
Built-in testing framework with excellent assertion messages. Testing is first-class in the Elixir ecosystem.
Mix is the build tool. Hex is the package manager. Both are excellent and built into the core toolchain.
Run Elixir on embedded hardware. Fault-tolerant IoT firmware with the full OTP supervision model.