Elixir Quick Reference for Experienced Programmers

For more see:

Language Basics

Data Types

# Atoms (like Ruby symbols)
:ok
:error
true                      # true/false/nil are atoms

# Numbers
42                        # Integer
3.14                      # Float

# Strings (UTF-8 binaries)
"hello"                   # Double quotes = string (binary)
'hello'                   # Single quotes = charlist (Erlang compat, rarely used)

# Tuples (fixed-size, contiguous memory)
{:ok, "result"}
{:error, "not found"}

# Lists (linked lists)
[1, 2, 3]
[head | tail] = [1, 2, 3]  # head = 1, tail = [2, 3]

# Keyword lists (list of {atom, value} tuples)
[name: "Tom", age: 30]   # Same as [{:name, "Tom"}, {:age, 30}]
# Allows duplicate keys, ordered — used for options

# Maps (key-value, any key type)
%{name: "Tom", age: 30}  # Atom keys (shorthand)
%{"name" => "Tom"}        # String keys

# Structs (typed maps with compile-time checks)
defmodule User do
  defstruct [:name, :email, active: true]
end
%User{name: "Tom", email: "tom@example.com"}

# Ranges
1..10
1..10//2                  # Step of 2 (Elixir 1.12+)

Pattern Matching

# The = operator is match, not assignment
{:ok, result} = {:ok, 42}    # result = 42
{:ok, result} = {:error, ""}  # ** (MatchError)

# In function heads
def process({:ok, data}), do: handle_data(data)
def process({:error, reason}), do: log_error(reason)

# In case
case HTTP.get(url) do
  {:ok, %{status: 200, body: body}} -> parse(body)
  {:ok, %{status: 404}} -> :not_found
  {:error, reason} -> {:error, reason}
end

# Pin operator (match against existing value, don't rebind)
x = 1
^x = 1    # Matches
^x = 2    # ** (MatchError)

# Destructuring maps
%{name: name, age: age} = %{name: "Tom", age: 30, extra: "ignored"}
# name = "Tom", age = 30

# Destructuring in function params
def greet(%User{name: name}), do: "Hello, #{name}"

Functions

Named Functions (in modules)

defmodule Math do
  def add(a, b), do: a + b           # Public

  defp validate(n) when n > 0, do: n  # Private
  defp validate(_), do: raise "must be positive"

  # Multi-clause with guards
  def divide(_, 0), do: {:error, :division_by_zero}
  def divide(a, b) when is_number(a) and is_number(b), do: {:ok, a / b}

  # Default arguments
  def increment(n, step \\ 1), do: n + step
end

Anonymous Functions

add = fn a, b -> a + b end
add.(1, 2)                    # => 3 (note the dot)

# Shorthand capture syntax
add = &(&1 + &2)
add.(1, 2)                    # => 3

double = &(&1 * 2)
Enum.map([1, 2, 3], double)  # => [2, 4, 6]

# Capture named function
Enum.map([1, 2, 3], &Math.add(&1, 1))
upcase = &String.upcase/1     # Capture with arity
Enum.map(["a", "b"], upcase) # => ["A", "B"]

Key Distinction: Named vs Anonymous

# Named function - called without dot
Math.add(1, 2)

# Anonymous function - called WITH dot
add = fn a, b -> a + b end
add.(1, 2)

The Pipe Operator

# Without pipe (nested calls)
String.split(String.trim(String.downcase("  HELLO WORLD  ")))

# With pipe (left-to-right, result becomes first arg)
"  HELLO WORLD  "
|> String.downcase()
|> String.trim()
|> String.split()
# => ["hello", "world"]

# Common pattern with Enum
users
|> Enum.filter(&(&1.active))
|> Enum.map(& &1.email)
|> Enum.sort()
|> Enum.take(10)

Control Flow

# if/else (rare in idiomatic Elixir — prefer pattern matching)
if condition do
  "yes"
else
  "no"
end

# unless
unless logged_in?, do: redirect(conn, to: "/login")

# cond (first truthy match)
cond do
  age < 13 -> "child"
  age < 18 -> "teenager"
  true -> "adult"         # Default clause
end

# case (pattern matching)
case value do
  {:ok, result} -> result
  {:error, :not_found} -> nil
  {:error, reason} -> raise reason
end

# with (happy path chaining — early exit on mismatch)
with {:ok, user} <- fetch_user(id),
     {:ok, profile} <- fetch_profile(user),
     {:ok, avatar} <- fetch_avatar(profile) do
  {:ok, %{user: user, avatar: avatar}}
else
  {:error, reason} -> {:error, reason}
end

Modules & Behaviours

defmodule Greeter do
  @moduledoc "Handles greetings"     # Module documentation
  @greeting "Hello"                   # Module attribute (compile-time constant)

  @doc "Greets a person by name"
  def greet(name), do: "#{@greeting}, #{name}!"
end

# Behaviours (like interfaces)
defmodule Parser do
  @callback parse(String.t()) :: {:ok, term()} | {:error, String.t()}
end

defmodule JSONParser do
  @behaviour Parser

  @impl Parser
  def parse(str), do: Jason.decode(str)
end

use, import, alias, require

defmodule MyModule do
  alias MyApp.Accounts.User         # User instead of MyApp.Accounts.User
  alias MyApp.Accounts.{User, Role} # Multiple aliases

  import Enum, only: [map: 2, filter: 2]  # Bring functions into scope
  import Enum, except: [split: 2]

  require Logger                    # Needed for macros (compile-time)

  use GenServer                     # Calls GenServer.__using__/1 macro
  # 'use' injects code — it's a macro that typically adds functions/behaviour
end

Enum & Stream

# Enum (eager — processes entire collection)
Enum.map([1, 2, 3], &(&1 * 2))              # => [2, 4, 6]
Enum.filter([1, 2, 3], &(&1 > 1))           # => [2, 3]
Enum.reject([1, 2, 3], &(&1 > 1))           # => [1]
Enum.reduce([1, 2, 3], 0, &(&1 + &2))       # => 6
Enum.any?([1, 2, 3], &(&1 > 2))             # => true
Enum.all?([1, 2, 3], &(&1 > 0))             # => true
Enum.find([1, 2, 3], &(&1 > 1))             # => 2
Enum.count([1, 2, 3])                        # => 3
Enum.sort_by(users, & &1.name)
Enum.group_by(users, & &1.role)
Enum.zip([1, 2], [:a, :b])                  # => [{1, :a}, {2, :b}]
Enum.flat_map([[1, 2], [3]], &Function.identity/1) # => [1, 2, 3]
Enum.chunk_every([1, 2, 3, 4, 5], 2)        # => [[1, 2], [3, 4], [5]]
Enum.each(items, &IO.inspect/1)              # Side effects (returns :ok)

# Stream (lazy — computes on demand)
1..1_000_000
|> Stream.filter(&(rem(&1, 2) == 0))
|> Stream.map(&(&1 * 2))
|> Enum.take(5)                              # Only computes 5 values
# => [4, 8, 12, 16, 20]

# Infinite streams
Stream.iterate(0, &(&1 + 1))                # 0, 1, 2, 3, ...
Stream.cycle([1, 2, 3])                      # 1, 2, 3, 1, 2, 3, ...
Stream.repeatedly(fn -> :rand.uniform() end) # Random numbers forever

Maps & Structs

# Map operations
map = %{name: "Tom", age: 30}
map.name                              # => "Tom" (atom key access)
map[:name]                            # => "Tom" (bracket access, returns nil for missing)
Map.get(map, :name)                   # => "Tom"
Map.get(map, :missing, "default")     # => "default"
Map.put(map, :email, "tom@test.com")  # Returns new map
Map.merge(map, %{age: 31})           # Returns new map
Map.delete(map, :age)                 # Returns new map
Map.keys(map)                         # => [:age, :name]
Map.values(map)                       # => [30, "Tom"]

# Update syntax (key must exist)
%{map | age: 31}                      # => %{name: "Tom", age: 31}
%{map | missing: "x"}                 # ** (KeyError)

# Nested access/update
users = %{tom: %{age: 30}}
get_in(users, [:tom, :age])           # => 30
put_in(users, [:tom, :age], 31)       # => %{tom: %{age: 31}}
update_in(users, [:tom, :age], &(&1 + 1))

# Structs
defmodule User do
  defstruct [:name, :email, active: true]

  def new(name, email) do
    %User{name: name, email: email}
  end
end

user = %User{name: "Tom"}
%User{name: name} = user              # Pattern match on struct type
user.active                            # => true
%{user | active: false}                # Update (same syntax as maps)

Processes & Concurrency

# Spawn a process
pid = spawn(fn -> IO.puts("Hello from process") end)

# Send and receive messages
pid = spawn(fn ->
  receive do
    {:greet, name} -> IO.puts("Hello, #{name}")
  end
end)

send(pid, {:greet, "Tom"})

# Self
self()                                # Current process PID

# Process links (crash together)
spawn_link(fn -> raise "boom" end)   # Crashes the caller too

# Monitors (notification on crash, no crash propagation)
ref = Process.monitor(pid)
receive do
  {:DOWN, ^ref, :process, ^pid, reason} -> IO.puts("Process died: #{reason}")
end

# Tasks (high-level async)
task = Task.async(fn -> expensive_work() end)
result = Task.await(task)             # Blocks until done (5s default timeout)

# Multiple tasks
tasks = Enum.map(urls, &Task.async(fn -> HTTP.get(&1) end))
results = Task.await_many(tasks)

GenServer (Generic Server)

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)
  def get_count, do: GenServer.call(__MODULE__, :get)

  # Server callbacks
  @impl true
  def init(initial), do: {:ok, initial}

  @impl true
  def handle_cast(:increment, count), do: {:noreply, count + 1}

  @impl true
  def handle_call(:get, _from, count), do: {:reply, count, count}
end

# Usage
{:ok, _pid} = Counter.start_link(0)
Counter.increment()
Counter.get_count()   # => 1

GenServer Call vs Cast

call cast
Synchronous? Yes (blocks caller) No (fire-and-forget)
Returns Reply value :ok
Callback handle_call/3 handle_cast/2
Use when You need the result You don't need a response

Supervisors

defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      {Counter, 0},                           # Child spec
      {MyApp.Worker, []},
      {Task.Supervisor, name: MyApp.TaskSupervisor},
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Supervision Strategies

Strategy Behavior
:one_for_one Only restart the failed child
:one_for_all Restart all children if one fails
:rest_for_one Restart the failed child and all children started after it

Error Handling

# Pattern matching on tagged tuples (preferred)
case File.read("data.txt") do
  {:ok, contents} -> process(contents)
  {:error, :enoent} -> IO.puts("File not found")
  {:error, reason} -> IO.puts("Error: #{reason}")
end

# try/rescue (rare — for unexpected errors)
try do
  risky_operation()
rescue
  e in RuntimeError -> IO.puts("Runtime error: #{e.message}")
  ArgumentError -> IO.puts("Bad argument")
after
  cleanup()
end

# raise / throw
raise "something went wrong"
raise ArgumentError, message: "bad value"

# Bang functions (raise on error)
File.read!("data.txt")               # Returns contents or raises
File.read("data.txt")                # Returns {:ok, contents} or {:error, reason}

Convention: ! Functions

# Functions often come in pairs:
File.read("f.txt")      # => {:ok, "..."} or {:error, reason}
File.read!("f.txt")     # => "..." or raises File.Error

Enum.fetch([1,2], 0)    # => {:ok, 1}
Enum.fetch!([1,2], 0)   # => 1

# Use non-bang when you want to handle the error
# Use bang when failure is unexpected / should crash

Strings & Sigils

# Interpolation
"Hello, #{name}"

# Multiline
"""
Hello,
World
"""

# Sigils
~s(string with "quotes")             # String
~r/regex pattern/                      # Regex
~w(foo bar baz)                        # Word list => ["foo", "bar", "baz"]
~w(foo bar baz)a                       # Atom word list => [:foo, :bar, :baz]
~D[2024-01-15]                         # Date
~T[13:45:00]                           # Time
~U[2024-01-15 13:45:00Z]              # UTC DateTime

# String functions
String.length("hello")                 # => 5
String.upcase("hello")                 # => "HELLO"
String.downcase("HELLO")              # => "hello"
String.trim("  hello  ")              # => "hello"
String.split("a,b,c", ",")           # => ["a", "b", "c"]
String.replace("hello", "l", "r")    # => "herro"
String.starts_with?("hello", "he")   # => true
String.contains?("hello", "ell")     # => true
String.to_integer("42")              # => 42

Comprehensions

# for (not a loop — returns a list by default)
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 (collect into different types)
for {k, v} <- %{a: 1, b: 2}, into: %{}, do: {k, v * 2}
# => %{a: 2, b: 4}

# Pattern matching in generators
for {:ok, val} <- [{:ok, 1}, {:error, 2}, {:ok, 3}], do: val
# => [1, 3] (errors silently skipped)

Protocols (Polymorphism)

# Define a protocol
defprotocol Printable do
  @doc "Converts data to a printable string"
  def to_str(data)
end

# Implement for different types
defimpl Printable, for: Integer do
  def to_str(n), do: Integer.to_string(n)
end

defimpl Printable, for: User do
  def to_str(user), do: user.name
end

# Usage
Printable.to_str(42)                  # => "42"
Printable.to_str(%User{name: "Tom"})  # => "Tom"

Testing (ExUnit)

# test/user_test.exs
defmodule UserTest do
  use ExUnit.Case, async: true        # async: true runs in parallel

  describe "full_name/1" do
    test "returns first and last name" do
      user = %User{first: "Tom", last: "Powell"}
      assert User.full_name(user) == "Tom Powell"
    end

    test "handles missing last name" do
      user = %User{first: "Tom", last: nil}
      assert User.full_name(user) == "Tom"
    end
  end

  test "raises on invalid input" do
    assert_raise ArgumentError, fn ->
      User.full_name(nil)
    end
  end
end

Common Assertions

assert value                           # Truthy
refute value                           # Falsy
assert x == y                          # Equality
assert x =~ ~r/pattern/               # Regex match
assert_raise RuntimeError, fn -> ... end
assert_receive {:msg, _}, 500         # Process message (with timeout)
refute_receive {:msg, _}

Mix (Build Tool)

mix new myapp                          # Create new project
mix new myapp --sup                    # With supervision tree
mix deps.get                           # Install dependencies
mix compile                            # Compile project
mix test                               # Run tests
mix test test/user_test.exs            # Run specific file
mix test test/user_test.exs:15         # Run specific line
mix format                             # Auto-format code
mix hex.info package_name              # Package info
iex -S mix                             # Interactive shell with project loaded

Dependencies (mix.exs)

# mix.exs
defmodule MyApp.MixProject do
  use Mix.Project

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.15",
      deps: deps()
    ]
  end

  def application do
    [
      extra_applications: [:logger],
      mod: {MyApp.Application, []}
    ]
  end

  defp deps do
    [
      {:jason, "~> 1.4"},              # JSON parser
      {:httpoison, "~> 2.0"},          # HTTP client
      {:ecto, "~> 3.10"},              # Database wrapper
      {:ex_machina, "~> 2.7", only: :test},  # Test factories
    ]
  end
end

Key Differences from Other Languages

That's the essential Elixir. The rest is exploring OTP patterns, the rich standard library, and the ecosystem of Hex packages.