What is Elixir?
Elixir is a dynamic, functional language designed for building scalable and maintainable applications. It runs on the Erlang VM (BEAM), known for running low-latency, distributed, and fault-tolerant systems.
Functional
Immutable data, first-class functions, and pattern matching. Code that's easy to reason about.
Concurrent
Lightweight processes, message passing, and the actor model. Scale to millions of connections.
Fault-Tolerant
Supervisors and "let it crash" philosophy. Build systems that self-heal from failures.
Extensible
Powerful macros and DSLs. Elixir is written in Elixir. Build tools like Phoenix and Ecto.
Basics
Hello World
# hello.exs IO.puts("Hello, World!") # Run with: elixir hello.exs
Interactive Shell (IEx)
# Start interactive Elixir iex # Start with your project iex -S mix # Compile and run elixir script.exs
Variables
# Variables are immutable (rebinding creates new value) x = 1 x = 2 # rebinding, not mutation # Naming conventions snake_case = "variables and functions" PascalCase # modules only _unused # underscore prefix for unused vars # Pin operator - match against existing value x = 1 ^x = 1 # matches (x is still 1) ^x = 2 # raises MatchError
Comments
# Single line comment # No multi-line comments # Just use multiple single-line comments
Data Types
Primitive Types
| Type | Example | Notes |
|---|---|---|
| Integer | 42, 1_000_000, 0xFF |
Arbitrary precision |
| Float | 3.14, 1.0e-10 |
64-bit double precision |
| Boolean | true, false |
Actually atoms |
| Atom | :ok, :error, nil |
Named constants |
| String | "hello" |
UTF-8 encoded binary |
| Charlist | 'hello' |
List of code points |
Atoms
# Atoms are constants whose value is their name :ok :error :hello_world # Module names are atoms Enum == :"Elixir.Enum" # true # nil, true, false are atoms nil == :nil # true true == :true # true
Strings
# Double-quoted strings (binaries) name = "world" "Hello, #{name}!" # interpolation # Multi-line strings (heredoc) """ Multi-line string """ # String functions String.length("hello") # 5 String.upcase("hello") # "HELLO" String.split("a,b,c", ",") # ["a", "b", "c"] String.replace("hello", "l", "r") # "herro" "hello" <> " world" # "hello world"
Collections
# List (linked list) list = [1, 2, 3] [0 | list] # [0, 1, 2, 3] - prepend [head | tail] = list # head = 1, tail = [2, 3] # Tuple (fixed size, contiguous memory) tuple = {:ok, "value"} elem(tuple, 1) # "value" # Map (key-value store) map = %{"name" => "Alice", "age" => 30"name"] # "Alice" # Map with atom keys (shorthand) map = %{name: "Alice", age: 30} map.name # "Alice" map[:name] # "Alice" # Keyword list (ordered, duplicate keys allowed) opts = [name: "Alice", age: 30] opts[:name] # "Alice"
Range
# Inclusive range 1..5 # 1, 2, 3, 4, 5 # Exclusive range 1...5 # 1, 2, 3, 4 (excludes 5) # Descending range 5..1//-1 # 5, 4, 3, 2, 1
Sigils
# String ~s(Hello "world") # "Hello \"world\"" ~S(No \n escapes) # "No \\n escapes" # Charlist ~c(hello) # 'hello' # Word list ~w(foo bar baz) # ["foo", "bar", "baz"] ~w(foo bar baz)a # [:foo, :bar, :baz] # Regex ~r/hello/i # case-insensitive regex Regex.match?(~r/foo/, "foobar") # true
Pattern Matching
The = operator is the match operator, not just assignment. It tries to make the left side match the right side.
Basic Matching
# Simple binding x = 1 # x = 1 1 = x # matches! (x is 1) 2 = x # MatchError! # Destructuring tuples {:ok, result} = {:ok, 42# 42 # Destructuring lists [head | tail] = [1, 2, 3] head # 1 tail # [2, 3] [a, b, c] = [1, 2, 3] [a, _, c] = [1, 2, 3] # ignore middle element
Map Matching
# Extract values from map %{name: name} = %{name: "Alice", age: 30} name # "Alice" # Match partial structure %{age: age} = %{name: "Alice", age: 30} age # 30
Pin Operator
x = 1 # Without pin: rebinds x {x, y} = {2, 3} # x = 2, y = 3 x = 1 # With pin: matches against x's value {^x, y} = {1, 3} # matches! y = 3 {^x, y} = {2, 3} # MatchError! (x is 1, not 2)
Function Clause Matching
defmodule Math do def zero?(0), do: true def zero?(_), do: false end Math.zero?(0) # true Math.zero?(1) # false
Control Flow
if / unless
# if expression if condition do "truthy" else "falsy" end # One-liner if condition, do: "yes", else: "no" # unless (negated if) unless condition, do: "falsy"
case
case value do {:ok, result} -> "Success: #{result}" {:error, reason} -> "Error: #{reason}" _ -> "Unknown" end # With guards case x do n when n < 0 -> "negative" n when n > 0 -> "positive" _ -> "zero" end
cond
# Multiple conditions (like else-if) cond do x > 10 -> "big" x > 5 -> "medium" true -> "small" # default case end
with
# Chain pattern matches (happy path) with {:ok, user} <- fetch_user(id), {:ok, posts} <- fetch_posts(user), {:ok, comments} <- fetch_comments(posts) do # All matched successfully {:ok, %{user: user, posts: posts, comments: comments|} else {:error, reason} -> {:error, reason} _ -> {:error, :unknown} end
for (Comprehensions)
# Basic list comprehension for x <- [1, 2, 3], do: x * 2 # [2, 4, 6] # With filter for x <- 1..10, rem(x, 2) == 0, do: x # [2, 4, 6, 8, 10] # Multiple generators (nested) for x <- [1, 2], y <- [:a, :b], do: {x, y} # [{1, :a}, {1, :b}, {2, :a}, {2, :b}] # Into different collection for {k, v} <- %{a: 1, b: 2}, into: %{}, do: {k, v * 2} # %{a: 2, b: 4}
Functions
Anonymous Functions
# Define with fn add = fn a, b -> a + b end add.(1, 2) # 3 (note the dot) # Shorthand capture syntax add = &(&1 + &2) add.(1, 2) # 3 # Multiple clauses handle = fn {:ok, result} -> "Success: #{result}" {:error, msg} -> "Error: #{msg}" end
Named Functions
defmodule Math do # Public function def add(a, b) do a + b end # One-liner def multiply(a, b), do: a * b # Private function defp helper(x), do: x * 2 end
Default Arguments
def greet(name, greeting \\ "Hello") do "#{greeting}, #{name}!" end greet("Alice") # "Hello, Alice!" greet("Alice", "Hi") # "Hi, Alice!"
Guards
def abs(x) when x >= 0, do: x def abs(x) when x < 0, do: -x # Guard expressions when is_integer(x) when is_binary(s) # strings are binaries when is_list(l) when is_map(m) when is_atom(a) when is_nil(x) when x in [1, 2, 3] when x > 0 and x < 10
Pipe Operator
# Transform data through a pipeline result = data |> parse() |> Enum.filter(&valid?/1) |> Enum.map(&transform/1) |> Enum.sort() # Pipes pass result as first argument "hello" |> String.upcase() |> String.reverse() # "OLLEH"
Function Capturing
# Capture named function Enum.map([1, 2, 3], &Math.double/1) # Capture with arity add = &Kernel.+/2 add.(1, 2) # 3 # Create partial application add_one = &(&1 + 1) add_one.(5) # 6
Modules & Structs
Defining Modules
defmodule MyApp.User do # Module attribute (compile-time constant) @default_role :member def new(name) do %{name: name, role: @default_role} end end
Structs
defmodule User do # Define struct with defaults defstruct [:name, :email, age: 0, active: true] # Enforce required keys @enforce_keys [:name, :email] end # Create struct user = %User{name: "Alice", email: "alice@example.com"} # Access fields user.name # "Alice" # Update struct %{user | age: 30# Pattern match on struct %User{name: name} = user
Protocols
# Define a protocol (interface) defprotocol Displayable do def display(data) end # Implement for a type defimpl Displayable, for: User do def display(user) do "User: #{user.name}" end end # Use protocol Displayable.display(user)
Behaviours
# Define a behaviour (interface for modules) defmodule Parser do @callback parse(String.t()) :: {:ok, term()} | {:error, String.t()} end # Implement behaviour defmodule JSONParser do @behaviour Parser @impl true def parse(json) do # implementation end end
Module Directives
defmodule MyModule do # Import functions from another module import Enum, only: [map: 2, filter: 2] # Alias a module alias MyApp.Accounts.User alias MyApp.Accounts.User, as: U # Require for macros require Logger # Use (invoke __using__ macro) use GenServer end
Enum & Stream
The Enum module provides functions for working with collections. Stream provides lazy equivalents.
Common Enum Functions
list = [1, 2, 3, 4, 5] # Transform Enum.map(list, &(&1 * 2)) # [2, 4, 6, 8, 10] Enum.filter(list, &(&1 > 2)) # [3, 4, 5] Enum.reject(list, &(&1 > 2)) # [1, 2] # Reduce Enum.reduce(list, 0, &(&1 + &2)) # 15 (sum) Enum.sum(list) # 15 Enum.product(list) # 120 # Search Enum.find(list, &(&1 > 3)) # 4 Enum.any?(list, &(&1 > 4)) # true Enum.all?(list, &(&1 > 0)) # true Enum.member?(list, 3) # true # Access Enum.at(list, 2) # 3 Enum.take(list, 3) # [1, 2, 3] Enum.drop(list, 2) # [3, 4, 5] Enum.first(list) # 1 Enum.last(list) # 5 # Sort & Organize Enum.sort([3, 1, 2]) # [1, 2, 3] Enum.sort_by(users, & &1.age) # sort by field Enum.reverse(list) # [5, 4, 3, 2, 1] Enum.uniq([1, 1, 2, 2]) # [1, 2] Enum.group_by(users, & &1.role) # group by field # Join Enum.join(["a", "b"], ",") # "a,b" Enum.zip([1, 2], [:a, :b]) # [{1, :a}, {2, :b}]
Stream (Lazy Evaluation)
# Streams are lazy - no computation until needed stream = 1..1_000_000 |> Stream.map(&(&1 * 2)) |> Stream.filter(&(rem(&1, 2) == 0)) |> Stream.take(10) # Force evaluation Enum.to_list(stream) # [4, 8, 12, 16, 20, 24, 28, 32, 36, 40] # Infinite streams Stream.iterate(0, &(&1 + 1)) # 0, 1, 2, 3, ... Stream.cycle([1, 2, 3]) # 1, 2, 3, 1, 2, 3, ... Stream.repeatedly(&:rand.uniform/0)
Map Operations
map = %{a: 1, b: 2, c: 3Map.get(map, :a) # 1
Map.get(map, :z, :default) # :default
Map.put(map, :d, 4) # %{a: 1, b: 2, c: 3, d: 4}
Map.delete(map, :a) # %{b: 2, c: 3}
Map.keys(map) # [:a, :b, :c]
Map.values(map) # [1, 2, 3]
Map.merge(map, %{d: 4}) # %{a: 1, b: 2, c: 3, d: 4}
Map.update!(map, :a, &(&1 + 10)) # %{a: 11, b: 2, c: 3}
Processes
Elixir processes are lightweight (not OS processes). They're isolated and communicate via message passing.
Spawning Processes
# Spawn a process pid = spawn(fn -> IO.puts("Hello from process!") end) # Check if alive Process.alive?(pid) # Get current process self() # returns PID # Spawn with link (crash together) spawn_link(fn -> raise "oops" end)
Message Passing
# Send message send(pid, {:hello, "world"}) # Receive messages receive do {:hello, msg} -> IO.puts("Got: #{msg}") _ -> IO.puts("Unknown message") after 5000 -> IO.puts("Timeout!") end
Simple Process Example
defmodule Counter do def start(initial \\ 0) do spawn(fn -> loop(initial) end) end defp loop(count) do receive do :increment -> loop(count + 1) {:get, caller} -> send(caller, {:count, count}) loop(count) end end end # Usage pid = Counter.start() send(pid, :increment) send(pid, {:get, self()}) receive do {:count, n} -> IO.puts("Count: #{n}") end
Tasks
# Async task task = Task.async(fn -> expensive_operation() end) result = Task.await(task) # blocks until done # Multiple concurrent tasks tasks = Enum.map(urls, fn url -> Task.async(fn -> fetch(url) end) end) results = Task.await_many(tasks)
Agents (Simple State)
# Start an agent with initial state {:ok, agent} = Agent.start_link(fn -> [] end) # Get state Agent.get(agent, fn list -> list end) # Update state Agent.update(agent, fn list -> [1 | list] end) # Get and update Agent.get_and_update(agent, fn list -> {length(list), [2 | list]} end)
GenServer
GenServer is a behaviour for implementing server processes. It handles sync/async calls, state management, and supervision.
Basic GenServer
defmodule Counter do use GenServer # Client API def start_link(initial \\ 0) do GenServer.start_link(__MODULE__, initial, name: __MODULE__) end def increment() do GenServer.cast(__MODULE__, :increment) end def get() do GenServer.call(__MODULE__, :get) end # Server Callbacks @impl true def init(initial) do {:ok, initial} end @impl true def handle_cast(:increment, count) do {:noreply, count + 1} end @impl true def handle_call(:get, _from, count) do {:reply, count, count} end end
GenServer Callbacks
| Callback | Triggered By | Returns |
|---|---|---|
init/1 |
start_link/3 |
{:ok, state} |
handle_call/3 |
GenServer.call/2 |
{:reply, response, state} |
handle_cast/2 |
GenServer.cast/2 |
{:noreply, state} |
handle_info/2 |
Any other message | {:noreply, state} |
terminate/2 |
Process shutdown | :ok |
Call vs Cast
# call - synchronous, blocks until reply result = GenServer.call(pid, :some_request) # cast - asynchronous, fire and forget GenServer.cast(pid, :some_request)
call when you need a response. Use cast for fire-and-forget operations where you don't need confirmation.
OTP & Supervisors
OTP (Open Telecom Platform) provides behaviours for building fault-tolerant applications. Supervisors monitor and restart failed processes.
Supervisor
defmodule MyApp.Supervisor do use Supervisor def start_link(opts) do Supervisor.start_link(__MODULE__, :ok, opts) end @impl true def init(:ok) do children = [ # Child specifications {Counter, 0}, {Cache, []}, %{ id: MyWorker, start: {MyWorker, :start_link, [[]]|} } ] Supervisor.init(children, strategy: :one_for_one) end end
Supervision Strategies
- :one_for_one
- Only the crashed child is restarted. Other children are unaffected.
- :one_for_all
- All children are restarted when one crashes. Use when children are interdependent.
- :rest_for_one
- Crashed child and all children started after it are restarted.
Application
defmodule MyApp.Application do use Application @impl true def start(_type, _args) do children = [ MyApp.Supervisor, {Phoenix.PubSub, name: MyApp.PubSub} ] opts = [strategy: :one_for_one, name: MyApp.Supervisor] Supervisor.start_link(children, opts) end end
Mix Build Tool
Project Commands
# Create new project mix new my_app mix new my_app --sup # with supervision tree # Dependencies mix deps.get # fetch dependencies mix deps.update --all # update all deps # Compile mix compile # Run mix run # run project mix run --no-halt # keep running iex -S mix # interactive with project # Test mix test mix test test/my_test.exs # specific file mix test --cover # with coverage # Format code mix format
mix.exs
defmodule MyApp.MixProject do use Mix.Project def project do [ app: :my_app, version: "0.1.0", elixir: "~> 1.19", deps: deps() ] end def application do [ extra_applications: [:logger], mod: {MyApp.Application, []} ] end defp deps do [ {:phoenix, "~> 1.7"}, {:ecto_sql, "~> 3.10"}, {:jason, "~> 1.4"} ] end end
Testing
# test/my_app_test.exs defmodule MyAppTest do use ExUnit.Case doctest MyApp describe "add/2" do test "adds two numbers" do assert MyApp.add(1, 2) == 3 end test "handles negative numbers" do assert MyApp.add(-1, 1) == 0 end end test "raises on invalid input" do assert_raise ArgumentError, fn -> MyApp.parse!(:invalid) end end end
Common Assertions
assert 1 + 1 == 2 refute 1 + 1 == 3 assert_raise RuntimeError, fn -> raise "boom" end assert_receive {:msg, _}, 1000 # wait up to 1s assert result =~ "pattern" # regex/string match