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:

FeatureListTuple
SizeDynamicFixed
AccessO(n)O(1)
UpdateCreate new listCreate new tuple
MemoryLinked nodesContiguous
Use caseVariable length collectionsFixed 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:

FeatureKeyword ListMap
KeysAtoms onlyAny type
OrderPreservedNot guaranteed
Duplicate keysAllowedNot allowed
PerformanceO(n)O(log n)
Use caseOptions/configData 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

  1. Create a map representing a book with title, author, and year
  2. Use pattern matching to extract the author from the book map
  3. Create a list of numbers 1-10 and use the pipe operator to:
    • Filter numbers greater than 5
    • Square each number
    • Sum the results
  4. 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.