Basics: Variables, Types, and Pattern Matching
Variables
In Elixir, "variables" are more like labels that bind to values. Due to immutability, you're always creating new data.
Variable Naming
# Valid names (snake_case)
user_name = "Alice"
user_age = 30
is_active = true
count_1 = 10
# Module attributes start with @
@module_constant 42
# Invalid names
User Name # No spaces
user-name # Hyphens not allowed
2_count # Can't start with number
Rebinding
x = 1
IO.puts(x) # => 1
x = 2 # Rebinding (not mutation)
IO.puts(x) # => 2
# The original value 1 hasn't changed - we just made x point to 2
Basic Types
Integers
# Regular integers
42
1_000_000 # Underscores for readability
# Binary, octal, hex
0b1010 # => 10 (binary)
0o777 # => 511 (octal)
0xFF # => 255 (hexadecimal)
# No limit on integer size
999_999_999_999_999_999_999_999_999
Floats
3.14
1.0e-10 # Scientific notation
# Must have at least one digit before and after decimal
1.0 # Valid
.5 # Invalid
5. # Invalid
Booleans
true
false
# Booleans are actually atoms
true == :true # => true
false == :false # => true
Atoms
Atoms are constants where their name is their value. Like symbols in Ruby or enums in other languages.
:ok
:error
:apple
:banana
# Atoms with spaces (quotes required)
:"hello world"
# Common atoms
nil # Special atom meaning "no value"
true # Atom
false # Atom
# Atoms are efficient - same atom stored once in memory
:hello == :hello # Same reference
Use cases:
- Function return statuses:
:ok,:error - Enum-like values:
:red,:green,:blue - Keys in keyword lists and maps
Strings
Strings in Elixir are UTF-8 encoded binaries.
"Hello, World!"
"Hello,
World!" # Multiline string
# String interpolation
name = "Alice"
"Hello, #{name}!" # => "Hello, Alice!"
# Escape sequences
"Line 1\nLine 2"
"Tab\there"
"Quote: \"Hello\""
# Heredocs (multiline strings)
message = """
This is a
multiline string
with proper indentation
"""
# String vs Charlist
"hello" # String (binary)
'hello' # Charlist (list of integers)
# These are different!
"hello" == 'hello' # => false
Common String Operations:
# Length
String.length("hello") # => 5
# Concatenation
"Hello" <> " " <> "World" # => "Hello World"
# Case conversion
String.upcase("hello") # => "HELLO"
String.downcase("HELLO") # => "hello"
String.capitalize("hello") # => "Hello"
# Trimming
String.trim(" hello ") # => "hello"
# Splitting
String.split("a,b,c", ",") # => ["a", "b", "c"]
# Contains?
String.contains?("hello", "ell") # => true
# Starts/ends with?
String.starts_with?("hello", "he") # => true
String.ends_with?("hello", "lo") # => true
# Replace
String.replace("hello", "l", "L") # => "heLLo"
# Slice
String.slice("hello", 1, 3) # => "ell"
Lists
Lists are linked lists - fast for prepending, slow for appending.
# Creating lists
[1, 2, 3, 4, 5]
["apple", "banana", "cherry"]
[1, "mixed", :types, true] # Can mix types
# Empty list
[]
# Prepend (fast - O(1))
[0 | [1, 2, 3]] # => [0, 1, 2, 3]
# Append (slow - O(n))
[1, 2, 3] ++ [4] # => [1, 2, 3, 4]
# Concatenation
[1, 2] ++ [3, 4] # => [1, 2, 3, 4]
# Subtraction
[1, 2, 3, 4] -- [2, 4] # => [1, 3]
# Head and tail
[head | tail] = [1, 2, 3, 4]
# head = 1
# tail = [2, 3, 4]
# Membership
3 in [1, 2, 3] # => true
5 in [1, 2, 3] # => false
# Length (slow - O(n))
length([1, 2, 3]) # => 3
Tuples
Tuples are fixed-size collections stored contiguously in memory - fast access, not for dynamic growth.
# Creating tuples
{1, 2, 3}
{"Alice", 30, :active}
# Common pattern: status tuples
{:ok, "Success"}
{:error, "Failed"}
# Accessing elements (0-indexed)
tuple = {:ok, 42, "message"}
elem(tuple, 0) # => :ok
elem(tuple, 1) # => 42
# Updating (creates new tuple)
put_elem(tuple, 1, 100) # => {:ok, 100, "message"}
# Size (fast - O(1))
tuple_size({1, 2, 3}) # => 3
List vs Tuple:
| Feature | List | Tuple |
|---|---|---|
| Size | Dynamic | Fixed |
| Access | O(n) | O(1) |
| Update | Create new list | Create new tuple |
| Memory | Linked nodes | Contiguous |
| Use case | Variable length collections | Fixed structured data |
Keyword Lists
Lists of 2-tuples where first element is an atom - used for options.
# These are equivalent
[{:name, "Alice"}, {:age, 30}]
[name: "Alice", age: 30]
# Duplicate keys allowed
[name: "Alice", name: "Bob"]
# Common for function options
String.split("a b c", " ", trim: true, parts: 2)
# Accessing
list = [name: "Alice", age: 30]
list[:name] # => "Alice"
Keyword.get(list, :age) # => 30
Maps
Key-value stores - use for structured data.
# Creating maps
%{"name" => "Alice", "age" => 30}
%{name: "Alice", age: 30} # Atom keys (common)
%{1 => "one", 2 => "two"} # Any type as key
# Empty map
%{}
# Accessing
user = %{name: "Alice", age: 30}
user[:name] # => "Alice"
user.name # => "Alice" (only works with atom keys)
# Updating (creates new map)
%{user | age: 31} # => %{name: "Alice", age: 31}
# Adding new keys
Map.put(user, :email, "alice@example.com")
# Pattern matching on maps
%{name: name} = %{name: "Alice", age: 30}
# name = "Alice"
# Nested update
user = %{profile: %{name: "Alice"}}
put_in(user.profile.name, "Bob")
# => %{profile: %{name: "Bob"}}
Keyword List vs Map:
| Feature | Keyword List | Map |
|---|---|---|
| Keys | Atoms only | Any type |
| Order | Preserved | Not guaranteed |
| Duplicate keys | Allowed | Not allowed |
| Performance | O(n) | O(log n) |
| Use case | Options/config | Data structures |
Pattern Matching
The = operator is the match operator, not assignment!
Basic Matching
# Match simple values
x = 1 # x matches 1
1 = x # 1 matches x (both work!)
# Match fails
1 = 2 # ** (MatchError)
# Match with atoms
:ok = :ok # Works
:ok = :error # ** (MatchError)
Tuple Matching
# Extract values from tuples
{status, message} = {:ok, "Success"}
# status = :ok
# message = "Success"
# Specific pattern
{:ok, result} = {:ok, 42}
# result = 42
{:ok, result} = {:error, "Failed"}
# ** (MatchError) - pattern doesn't match
# Ignore values with _
{:ok, _} = {:ok, 42} # Don't care about the value
# Multiple elements
{:ok, code, message} = {:ok, 200, "OK"}
List Matching
# Head and tail
[head | tail] = [1, 2, 3, 4]
# head = 1
# tail = [2, 3, 4]
# Multiple elements
[first, second | rest] = [1, 2, 3, 4, 5]
# first = 1
# second = 2
# rest = [3, 4, 5]
# Exact match
[1, 2, 3] = [1, 2, 3] # Works
[1, 2] = [1, 2, 3] # ** (MatchError)
# Pattern with specific values
[1, x, 3] = [1, 2, 3]
# x = 2
Map Matching
# Extract specific keys
%{name: name} = %{name: "Alice", age: 30}
# name = "Alice"
# Multiple keys
%{name: name, age: age} = %{name: "Alice", age: 30}
# Map can have extra keys
%{name: name} = %{name: "Alice", age: 30, email: "alice@example.com"}
# Works! name = "Alice"
# But must have matched keys
%{name: name} = %{age: 30}
# ** (MatchError) - no :name key
Pin Operator (^)
Use ^ to match against existing value instead of rebinding.
x = 1
# Without pin - rebinding
x = 2
# x is now 2
# With pin - matching
x = 1
^x = 1 # Works - matches existing value
^x = 2 # ** (MatchError) - doesn't match
# Practical example
user_id = 42
case get_user() do
%{id: ^user_id, name: name} ->
"Found user: #{name}"
_ ->
"Different user"
end
Operators
Arithmetic Operators
1 + 2 # => 3
5 - 3 # => 2
3 * 4 # => 12
10 / 2 # => 5.0 (always returns float)
div(10, 3) # => 3 (integer division)
rem(10, 3) # => 1 (remainder)
Comparison Operators
1 == 1 # => true
1 != 2 # => true
1 < 2 # => true
1 <= 1 # => true
1 > 0 # => true
1 >= 1 # => true
# Strict equality (checks type)
1 == 1.0 # => true
1 === 1.0 # => false
# Type ordering (different types can be compared)
1 < :atom # => true
# number < atom < reference < function < port < pid < tuple < map < list < bitstring
Boolean Operators
# Strict (require booleans)
true and false # => false
true or false # => true
not true # => false
# Relaxed (accept any type)
nil || false || 5 # => 5 (returns first truthy)
nil && false && 5 # => nil (returns first falsy)
!nil # => true
# Truthiness: only nil and false are falsy
# Everything else is truthy (including 0, "", [])
String Operators
"Hello" <> " " <> "World" # => "Hello World"
# String interpolation
name = "Alice"
"Hello, #{name}!" # => "Hello, Alice!"
# Complex expressions in interpolation
"Result: #{1 + 2}" # => "Result: 3"
List Operators
[1, 2] ++ [3, 4] # => [1, 2, 3, 4] (concatenation)
[1, 2, 3] -- [2] # => [1, 3] (subtraction)
Membership Operator
1 in [1, 2, 3] # => true
:name in [:name, :age] # => true
5 in [1, 2, 3] # => false
Pipe Operator
Chain function calls - output of left becomes first argument of right.
# Without pipe
String.upcase(String.trim(" hello "))
# With pipe (more readable)
" hello "
|> String.trim()
|> String.upcase()
# => "HELLO"
# Multi-step transformation
[1, 2, 3, 4, 5]
|> Enum.filter(fn x -> x > 2 end)
|> Enum.map(fn x -> x * 2 end)
|> Enum.sum()
# => 24
Type Checking
# Check types
is_integer(42) # => true
is_float(3.14) # => true
is_boolean(true) # => true
is_atom(:hello) # => true
is_binary("hello") # => true (strings are binaries)
is_list([1, 2]) # => true
is_tuple({1, 2}) # => true
is_map(%{a: 1}) # => true
is_function(fn -> :ok end) # => true
# Nil check
is_nil(nil) # => true
is_nil(false) # => false
Immutability in Practice
# Lists
list = [1, 2, 3]
new_list = [0 | list]
# list is still [1, 2, 3]
# new_list is [0, 1, 2, 3]
# Maps
user = %{name: "Alice", age: 30}
older_user = %{user | age: 31}
# user is still %{name: "Alice", age: 30}
# older_user is %{name: "Alice", age: 31}
# You can rebind the same variable
user = %{name: "Alice", age: 30}
user = %{user | age: 31}
# Now user refers to the new map
# But the old map still exists if referenced elsewhere
Practical Examples
Processing User Data
# Parse user input
{:ok, user} = {:ok, %{name: "Alice", age: 30, role: :admin}}
# Extract and transform
%{name: name, age: age, role: role} = user
greeting = "Hello, #{name}! You are #{age} years old."
Working with Lists
# Filter and transform
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
even_numbers =
numbers
|> Enum.filter(fn n -> rem(n, 2) == 0 end)
|> Enum.map(fn n -> n * 2 end)
# => [4, 8, 12, 16, 20]
Nested Data Structures
# Complex data
user = %{
name: "Alice",
address: %{
street: "123 Main St",
city: "Springfield"
},
hobbies: ["reading", "coding"]
}
# Access nested data
user.address.city # => "Springfield"
List.first(user.hobbies) # => "reading"
# Pattern match nested
%{address: %{city: city}} = user
# city = "Springfield"
Exercises
- Create a map representing a book with title, author, and year
- Use pattern matching to extract the author from the book map
- Create a list of numbers 1-10 and use the pipe operator to:
- Filter numbers greater than 5
- Square each number
- Sum the results
- Write expressions to check if a variable is a string, and another to check if it's an empty list
# Solutions
# 1. Book map
book = %{title: "1984", author: "George Orwell", year: 1949}
# 2. Pattern match author
%{author: author} = book
# author = "George Orwell"
# 3. Pipeline
result =
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
|> Enum.filter(fn x -> x > 5 end)
|> Enum.map(fn x -> x * x end)
|> Enum.sum()
# => 355
# 4. Type checks
is_binary("hello") # => true (strings are binaries)
[] == [] # => true
length([]) == 0 # => true
Next Steps
Now that you understand variables, types, and pattern matching, continue to 03-control-flow.md to learn about conditionals, case expressions, and loops in Elixir.