Basics
Welcome to your Elixir crash course! This guide will take you through the most important aspects of Elixir that you’ll use every day, while building a solid foundation that lets you explore more advanced topics on your own later.
What is Elixir and Why Should You Care?
Elixir is a functional programming language created by José Valim in 2011. It runs on the Erlang Virtual Machine (BEAM) and inherits Erlang’s powerful features for building distributed, fault-tolerant systems. The language combines Erlang’s battle-tested reliability with a more approachable, modern syntax that many developers find reminiscent of Ruby.
What makes Elixir special is its combination of powerful features:
- Scalability: Elixir can handle millions of concurrent connections efficiently, making it perfect for high-traffic web applications and real-time systems.
- Fault tolerance: Elixir systems are designed to be self-healing. When something goes wrong, the system can recover automatically without taking everything down with it.
- Functional programming: By treating data as immutable and emphasizing pure functions, Elixir helps you write code that’s easier to understand, test, and maintain.
- Developer happiness: The syntax is clean and expressive, and the tooling (compiler, formatter, test framework) is excellent out of the box.
Getting Started with Elixir
Let’s begin by setting up Elixir on your system.
Installation
The installation process varies by operating system:
For macOS users:
brew install elixir
For Ubuntu/Debian Linux users:
sudo apt-get update
sudo apt-get install elixir
For Windows users:
- Visit elixir-lang.org and click on the “Install” link
- Download the Windows installer
- Run the installer and follow the on-screen instructions
After installation, verify everything is working by opening a terminal and typing:
elixir -v
You should see output showing both the Erlang and Elixir versions installed on your system.
Setting Up Your Development Environment
For the best development experience, I recommend Visual Studio Code with the ElixirLS extension:
- Install Visual Studio Code if you don’t already have it
- Open the Extensions panel (Ctrl+Shift+X on Windows/Linux or Cmd+Shift+X on Mac)
- Search for “ElixirLS” and install it
This will give you helpful features like syntax highlighting, code completion, and inline error checking as you write Elixir code.
The Interactive Elixir Shell (IEx)
One of the best ways to learn Elixir is by experimenting in its interactive shell, called IEx. Think of it as a playground where you can run Elixir code and see the results immediately.
To start IEx, open a terminal and type:
iex
You’ll be greeted with a prompt that looks like iex(1)>
. Let’s try some basic expressions:
iex(1)> 2 + 3
5
iex(2)> "hello" <> " world" # String concatenation
"hello world"
iex(3)> String.upcase("elixir")
"ELIXIR"
This interactive environment is perfect for testing small pieces of code and learning how Elixir works. When you’re done, you can exit IEx by pressing Ctrl+C twice.
Understanding Elixir’s Basic Data Types
Let’s explore the fundamental building blocks of Elixir programs.
Numbers
Elixir supports integers and floating-point numbers:
iex(1)> 42 # integer
42
iex(2)> 3.14 # float
3.14
iex(3)> 1.0e-10 # scientific notation
1.0e-10
iex(4)> 1_000_000 # underscores for readability
1000000
All the usual arithmetic operations work as expected: +
, -
, *
, /
. Integer division is done with div/2
and remainder with rem/2
:
iex(5)> div(10, 3) # Integer division
3
iex(6)> rem(10, 3) # Remainder (modulo)
1
Strings
Strings in Elixir are enclosed in double quotes and are UTF-8 encoded by default:
iex(1)> "Hello, world!"
"Hello, world!"
iex(2)> "Hello, " <> "Elixir" # String concatenation
"Hello, Elixir"
You can also use string interpolation to embed expressions inside strings:
iex(3)> name = "Elixir"
"Elixir"
iex(4)> "Hello, #{name}!" # The expression inside #{} is evaluated
"Hello, Elixir!"
The String
module provides many useful functions for working with strings:
iex(5)> String.length("Elixir")
6
iex(6)> String.split("hello world", " ")
["hello", "world"]
iex(7)> String.upcase("elixir")
"ELIXIR"
Atoms
Atoms are constants where the name is the value itself. They’re similar to symbols in Ruby or enumeration values in other languages:
iex(1)> :hello
:hello
iex(2)> :success == :success
true
iex(3)> :success == :failure
false
Atoms are commonly used for keys in maps and for expressing constant values in your code. They’re particularly useful in pattern matching and when you need a set of well-defined values.
Booleans and nil
Elixir has true
and false
boolean values, and nil
which represents “nothing” (similar to null
in other languages):
iex(1)> true && false # Logical AND
false
iex(2)> true || false # Logical OR
true
iex(3)> !true # Logical NOT
false
iex(4)> nil # Represents "nothing"
nil
iex(5)> is_nil(nil)
true
Pattern Matching: Elixir’s Secret Weapon
Pattern matching is one of Elixir’s most powerful features. At first, it might seem strange if you’re coming from other programming languages, but it quickly becomes an essential tool in your Elixir programming toolkit.
In Elixir, the =
operator is not just for assignment—it’s a match operator:
iex(1)> x = 1 # This binds the value 1 to variable x
1
iex(2)> 1 = x # This matches the value of x against 1 (succeeds)
1
iex(3)> 2 = x # This tries to match x against 2 (fails)
** (MatchError) no match of right hand side value: 1
Where pattern matching really shines is in destructuring complex data:
iex(1)> {a, b, c} = {:hello, 42, "world"}
{:hello, 42, "world"}
iex(2)> a # Now a is bound to :hello
:hello
iex(3)> b # b is bound to 42
42
iex(4)> c # c is bound to "world"
"world"
You can use the underscore (_
) as a wildcard when you don’t care about certain values:
iex(5)> {_, age, _} = {:hello, 42, "world"} # Ignore first and last values
{:hello, 42, "world"}
iex(6)> age
42
Pattern matching becomes even more powerful when working with lists:
iex(7)> [first, second | rest] = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
iex(8)> first
1
iex(9)> second
2
iex(10)> rest
[3, 4, 5]
Working with Collections
Elixir provides several collection types, each with its own strengths.
Lists
Lists in Elixir are implemented as linked lists, which makes them efficient for prepending elements and recursively processing from left to right:
iex(1)> numbers = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
# Pattern matching with lists
iex(2)> [head | tail] = numbers
[1, 2, 3, 4, 5]
iex(3)> head # First element
1
iex(4)> tail # All remaining elements
[2, 3, 4, 5]
# Adding elements
iex(5)> [0 | numbers] # Prepend (fast operation)
[0, 1, 2, 3, 4, 5]
iex(6)> numbers ++ [6] # Append (slower for large lists)
[1, 2, 3, 4, 5, 6]
# Basic operations
iex(7)> length(numbers) # Get list length
5
iex(8)> Enum.at(numbers, 2) # Get element at index 2 (zero-based)
3
Lists can contain elements of different types:
iex(9)> mixed = [1, :two, "three", 4.0]
[1, :two, "three", 4.0]
Tuples
Tuples are similar to lists but are stored contiguously in memory. They’re good for fixed-size collections where you need fast access to elements by index:
iex(1)> person = {"John", 28, :developer}
{"John", 28, :developer}
iex(2)> elem(person, 0) # Access by index (zero-based)
"John"
iex(3)> elem(person, 1)
28
iex(4)> tuple_size(person) # Get tuple size
3
iex(5)> put_elem(person, 1, 29) # Create a new tuple with updated value
{"John", 29, :developer}
Maps
Maps are key-value stores, similar to dictionaries or hash maps in other languages:
# Map with string keys
iex(1)> person = %{"name" => "John", "age" => 28, "job" => "developer"}
%{"age" => 28, "job" => "developer", "name" => "John"}
iex(2)> person["name"] # Access with bracket notation
"John"
# Map with atom keys (more common in Elixir code)
iex(3)> person = %{name: "John", age: 28, job: "developer"}
%{age: 28, job: "developer", name: "John"}
iex(4)> person.name # Access with dot notation (only works with atom keys)
"John"
iex(5)> Map.get(person, :age) # Alternative access method
28
# Adding or updating values
iex(6)> Map.put(person, :location, "New York") # Add a new key-value pair
%{age: 28, job: "developer", location: "New York", name: "John"}
iex(7)> %{person | age: 29} # Update an existing value
%{age: 29, job: "developer", name: "John"}
Remember that in Elixir, all data is immutable. These operations don’t modify the original map—they return new maps with the requested changes.
Structs: Maps with a Schema
Structs are special maps with a defined structure and default values:
defmodule Person do
defstruct name: "", age: 0, job: nil
end
iex(1)> john = %Person{name: "John", age: 28, job: "developer"}
%Person{age: 28, job: "developer", name: "John"}
iex(2)> john.name
"John"
iex(3)> %{john | age: 29} # Update a field
%Person{age: 29, job: "developer", name: "John"}
Structs enforce their structure—you can’t add fields that weren’t defined in the struct:
iex(4)> %Person{name: "John", age: 28, location: "New York"}
** (KeyError) key :location not found
Functions and Modules
In Elixir, code is organized into modules containing functions. Functions are the core building blocks of your programs.
Creating Modules and Functions
Here’s how to define a simple module with some functions:
# Save as math.ex
defmodule Math do
# Regular function definition
def sum(a, b) do
a + b
end
# One-line function syntax
def multiply(a, b), do: a * b
# Private function (only callable within the module)
defp square(x), do: x * x
# Function using a private helper
def sum_of_squares(a, b) do
square(a) + square(b)
end
end
To use this module, you first need to compile it:
elixirc math.ex
Then you can use it in IEx:
iex(1)> Math.sum(2, 3)
5
iex(2)> Math.multiply(4, 5)
20
iex(3)> Math.sum_of_squares(3, 4)
25
iex(4)> Math.square(5) # This will fail because square is private
** (UndefinedFunctionError) function Math.square/1 is undefined or private
Anonymous Functions
Anonymous functions don’t have names and can be assigned to variables:
iex(1)> add = fn a, b -> a + b end
#Function<...>
iex(2)> add.(5, 3) # Note the dot for calling anonymous functions
8
# Shorthand syntax with capture operator
iex(3)> multiply = &(&1 * &2) # &1, &2 refer to the first and second arguments
#Function<...>
iex(4)> multiply.(4, 5)
20
# Capturing named functions
iex(5)> upcase = &String.upcase/1 # /1 indicates arity (number of arguments)
&String.upcase/1
iex(6)> upcase.("hello")
"HELLO"
Multiple Function Clauses
One of Elixir’s most powerful features is the ability to define multiple implementations of a function based on pattern matching:
defmodule Factorial do
def of(0), do: 1
def of(n) when n > 0, do: n * of(n - 1)
end
iex(1)> Factorial.of(5)
120
When you call Factorial.of(5)
, Elixir tries to match the argument against each function clause in order until it finds a match. This pattern is commonly used for recursion, as shown above, and for handling different input types or cases.
The Heart of Functional Programming
Now that we’ve covered the basics, let’s explore some of the functional programming concepts that make Elixir special.
Immutability: Why It Matters
In Elixir, all data is immutable—once created, it cannot be changed. Operations that seem to modify data are actually creating new copies with the requested changes:
iex(1)> list = [1, 2, 3]
[1, 2, 3]
iex(2)> new_list = [0 | list] # Creates a new list with 0 prepended
[0, 1, 2, 3]
iex(3)> list # The original list remains unchanged
[1, 2, 3]
This immutability brings several benefits:
- Predictability: Functions with the same input always produce the same output
- Concurrency safety: No need to worry about data races or locks
- Easier reasoning: You know that data won’t change unexpectedly
The Pipe Operator: Enhancing Readability
The pipe operator (|>
) passes the result of one function as the first argument to the next function. This allows you to chain operations in a clean, readable way:
iex(1)> "elixir is fun"
|> String.upcase()
|> String.split(" ")
["ELIXIR", "IS", "FUN"]
Without pipes, the same code would look like this:
iex(2)> String.split(String.upcase("elixir is fun"), " ")
["ELIXIR", "IS", "FUN"]
As operations get more complex, pipes keep your code readable by showing the transformation steps in a clear, sequential order.
The Enum Module: Collection Processing
The Enum
module provides a rich set of functions for working with collections:
iex(1)> numbers = [1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
# Map: transform each element
iex(2)> Enum.map(numbers, fn x -> x * 2 end)
[2, 4, 6, 8, 10]
# Filter: keep elements matching a condition
iex(3)> Enum.filter(numbers, fn x -> rem(x, 2) == 0 end)
[2, 4]
# Reduce: accumulate a result
iex(4)> Enum.reduce(numbers, 0, fn x, acc -> x + acc end)
15 # sum of all elements
# Combined with pipes for a data processing pipeline
iex(5)> numbers
|> Enum.filter(fn x -> rem(x, 2) == 0 end) # keep even numbers
|> Enum.map(fn x -> x * 2 end) # double them
|> Enum.sum() # sum the results
12
This functional approach to collection processing is a cornerstone of Elixir programming.
List Comprehensions: Concise List Generation
List comprehensions provide a concise way to generate lists based on existing collections:
# Generate a list of doubled even numbers from 1 to 10
iex(1)> for x <- 1..10, rem(x, 2) == 0, do: x * 2
[4, 8, 12, 16, 20]
# Comprehensions work with any enumerable, not just ranges
iex(2)> for word <- ["hello", "world", "elixir"], do: String.upcase(word)
["HELLO", "WORLD", "ELIXIR"]
Control Flow in Elixir
Elixir provides several constructs for controlling the flow of your programs.
Pattern Matching in Function Clauses
We’ve already seen how pattern matching in function definitions creates elegant control flow:
defmodule Greeting do
def say(:morning), do: "Good morning!"
def say(:evening), do: "Good evening!"
def say(_), do: "Hello!" # Default case
end
iex(1)> Greeting.say(:morning)
"Good morning!"
iex(2)> Greeting.say(:evening)
"Good evening!"
iex(3)> Greeting.say(:afternoon)
"Hello!"
Case: Pattern Matching on Values
The case
construct lets you pattern match on a value with multiple clauses:
iex(1)> grade = 85
85
iex(2)> case grade do
...> g when g >= 90 -> "A"
...> g when g >= 80 -> "B"
...> g when g >= 70 -> "C"
...> g when g >= 60 -> "D"
...> _ -> "F" # Default case
...> end
"B"
Cond: Selecting the First True Condition
The cond
construct chooses the first clause that evaluates to true:
iex(1)> age = 18
18
iex(2)> cond do
...> age < 13 -> "Child"
...> age < 18 -> "Teenager"
...> age < 65 -> "Adult"
...> true -> "Senior" # Default case (always matches)
...> end
"Adult"
If and Unless: Simple Conditionals
For simple conditions, Elixir provides if
and unless
:
iex(1)> if age >= 18 do
...> "You can vote"
...> else
...> "You cannot vote yet"
...> end
"You can vote"
iex(2)> unless File.exists?("file.txt") do
...> "File not found"
...> end
"File not found"
Mix: Your Project Management Tool
Mix is Elixir’s build tool for creating, compiling, testing, and managing dependencies for your projects.
Creating a New Project
To create a new Elixir project, run:
mix new my_app
cd my_app
This creates a directory structure like this:
my_app/
├── .formatter.exs # Formatter configuration
├── .gitignore
├── README.md
├── lib/ # Your application code goes here
│ └── my_app.ex
├── mix.exs # Project configuration
└── test/ # Test files
├── my_app_test.exs
└── test_helper.exs
Managing Dependencies
To add external libraries to your project, edit the deps
function in your mix.exs
file:
defp deps do
[
{:jason, "~> 1.4"}, # JSON library
{:httpoison, "~> 2.0"} # HTTP client
]
end
Then install the dependencies with:
mix deps.get
Common Mix Tasks
Mix provides many useful commands for working with your project:
mix compile # Compile the project
mix test # Run tests
mix format # Format code according to style guide
mix deps.update # Update dependencies
mix help # List available tasks
Running Your Application
There are a few ways to run your Elixir application:
# Start an interactive Elixir shell with your application loaded
iex -S mix
# Run a specific script
mix run my_script.exs
Concurrency: Elixir’s Superpower
One of Elixir’s most powerful features is its concurrency model, inherited from Erlang. It’s based on lightweight processes (not OS processes) that communicate via message passing.
Spawning Processes
Creating a new process is as simple as calling spawn/1
with a function:
iex(1)> pid = spawn(fn ->
...> IO.puts("Hello from process #{inspect self()}")
...> end)
Hello from process #PID<0.108.0>
#PID<0.108.0>
Elixir processes are extremely lightweight—you can create thousands or even millions of them without significantly impacting system performance.
Sending and Receiving Messages
Processes communicate by sending and receiving messages:
iex(1)> receiver = spawn(fn ->
...> receive do
...> {:hello, sender} ->
...> IO.puts("Got hello from #{inspect sender}")
...> send(sender, :hello_back)
...> end
...> end)
#PID<0.109.0>
# Send a message to the receiver process
iex(2)> send(receiver, {:hello, self()})
Got hello from #PID<0.106.0>
{:hello, #PID<0.106.0>}
# Wait to receive a response
iex(3)> receive do
...> :hello_back -> "They said hello back!"
...> end
"They said hello back!"
Understanding Process Flow
Let’s visualize how processes communicate:
sequenceDiagram participant MainProcess participant Worker MainProcess->>Worker: spawn(function) Note right of Worker: Process starts executing MainProcess->>Worker: send message Worker->>Worker: receive message Worker->>MainProcess: send response MainProcess->>MainProcess: receive response
Building a Simple Process Server
Here’s a more complete example of a process that maintains state—a simple counter:
defmodule Counter do
# Start the counter process
def start do
spawn(fn -> loop(0) end)
end
# Client API
def increment(pid) do
send(pid, :increment)
end
def get_value(pid) do
send(pid, {:get_value, self()})
receive do
{:count, value} -> value
end
end
# Server implementation
defp loop(count) do
receive do
:increment ->
# Increment and continue with new state
loop(count + 1)
{:get_value, from} ->
# Send current count back to caller
send(from, {:count, count})
# Continue with same state
loop(count)
end
end
end
You can use this counter like this:
# Start a counter process
iex(1)> counter = Counter.start()
#PID<0.112.0>
# Increment a few times
iex(2)> Counter.increment(counter)
:increment
iex(3)> Counter.increment(counter)
:increment
# Get the current value
iex(4)> Counter.get_value(counter)
2
This pattern—a process that loops and maintains state by calling itself recursively with updated state—is fundamental to Elixir applications. More advanced versions of this pattern are provided by OTP (Open Telecom Platform), Erlang’s framework for building robust, distributed systems.
A Glimpse of Phoenix: Elixir’s Web Framework
Phoenix is the most popular web framework for Elixir. While a complete Phoenix tutorial would go beyond our 85% target, here’s a quick introduction to get you started.
Creating a Phoenix Project
First, make sure you have Phoenix installed:
# Install Hex package manager if not installed
mix local.hex
# Install Phoenix
mix archive.install hex phx_new
# Create a new Phoenix project
mix phx.new my_app
cd my_app
# Set up the database
mix ecto.create
# Start the server
mix phx.server
Once the server is running, you can visit http://localhost:4000
in your browser to see your Phoenix application.
Understanding Phoenix’s Request Flow
Here’s how a request flows through a Phoenix application:
graph TD A[Browser Request] -->|HTTP| B[Endpoint] B -->|Router| C[Controller] C -->|Data| D[View] D -->|Template| E[HTML Response] C -->|Optional| F[Database] F -->|Results| C
Creating a Simple Phoenix Controller
Controllers handle incoming requests and prepare data for views:
# lib/my_app_web/controllers/hello_controller.ex
defmodule MyAppWeb.HelloController do
use MyAppWeb, :controller
def index(conn, _params) do
render(conn, :index)
end
def show(conn, %{"name" => name}) do
render(conn, :show, name: name)
end
end
Setting Up Routes
The router maps URLs to controller actions:
# lib/my_app_web/router.ex
defmodule MyAppWeb.Router do
use MyAppWeb, :router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
end
scope "/", MyAppWeb do
pipe_through :browser
get "/", PageController, :index
get "/hello", HelloController, :index
get "/hello/:name", HelloController, :show
end
end
The Remaining 15%: Your Path Forward
You’ve now learned about 85% of Elixir that you’ll use in everyday programming. The remaining 15% consists of more advanced topics that you can explore as your needs evolve. Here’s a road map to those advanced concepts:
OTP (Open Telecom Platform)
- GenServer for stateful processes
- Supervisors for fault tolerance
- Application structure and lifecycle
Macros and Metaprogramming
- Extending Elixir’s syntax
- Abstract Syntax Tree (AST) manipulation
- Building domain-specific languages
Protocols and Behaviours
- Implementing polymorphism in Elixir
- Creating custom protocols
- Defining consistent interfaces with behaviours
Advanced Phoenix Features
- LiveView for real-time UIs without JavaScript
- Channels for WebSockets
- PubSub for event broadcasting
Distribution and Clustering
- Connecting Elixir nodes across machines
- Distributed process registry
- Building systems that survive network partitions
Hot Code Swapping
- Updating running applications without downtime
Interoperability
- Native Implemented Functions (NIFs)
- Ports and Port Drivers
- Interfacing with other languages
Advanced Testing Techniques
- Property-based testing
- Testing concurrent code
- Integration testing
Performance Optimization
- Benchmarking
- Memory profiling
- Optimization techniques
Deployment Strategies
- Creating releases
- Docker containerization
- Clustering in production
Visualizing Elixir’s Architecture
Here’s how an Elixir application is typically structured:
graph TD A[Application] --> B[Supervision Tree] B --> C[Supervisor] B --> D[Supervisor] C --> E[Worker Process] C --> F[Worker Process] D --> G[Worker Process] E ---|Messages| F F ---|Messages| G H[Erlang VM/BEAM] --> B I[Operating System] --> H
Conclusion: Your Journey with Elixir
This crash course has covered the essential 85% of Elixir that you’ll use in your daily programming. We’ve explored the language’s functional nature, immutability, and concurrency model—key concepts that make Elixir powerful and unique.
As you grow more comfortable with these fundamentals, you’ll naturally begin to explore the advanced topics in the “remaining 15%” as your projects require them. The more you use Elixir, the more you’ll appreciate its elegant design and powerful capabilities.
Resources for continued learning:
- Elixir Official Documentation - Comprehensive and well-written
- Elixir School - Free, community-driven lessons
- Elixir Forum - Friendly community for questions and discussions
- Exercism’s Elixir Track - Practice exercises to build your skills
Remember that the functional programming paradigm might require an adjustment if you’re coming from object-oriented languages, but the benefits in code clarity, maintainability, and scalability are well worth the learning curve.
Happy coding with Elixir!