Intermediate

Build on your Elixir foundations with 30 intermediate examples covering advanced patterns, practical OTP usage, error handling, and testing strategies. Each example is self-contained and heavily annotated.

Example 31: Guards in Depth

Guards are boolean expressions that add additional constraints to pattern matches in function heads, case clauses, and other contexts. They enable more precise pattern matching based on types and values.

Code:

defmodule Guards do
  # Type guards (dispatch based on runtime type)
  def type_check(value) when is_integer(value), do: "integer: #{value}"
  # => when is_integer(value): guard checks type at runtime
  # => Guard fails: tries next clause
  # => Guard succeeds: executes this clause
  def type_check(value) when is_float(value), do: "float: #{value}"
  # => Multiple clauses tried top-to-bottom
  def type_check(value) when is_binary(value), do: "string: #{value}"
  # => is_binary checks for bitstring (includes strings)
  def type_check(value) when is_atom(value), do: "atom: #{inspect(value)}"
  # => inspect/1 converts atom to readable string
  def type_check(_value), do: "unknown type"
  # => Catch-all clause (no guard = always matches)
  # => _ prefix signals "intentionally unused"

  # Value guards (dispatch based on value ranges)
  def category(age) when age < 13, do: "child"
  # => Numeric comparison in guard
  def category(age) when age >= 13 and age < 20, do: "teen"
  # => Compound guard: and combines conditions
  # => Both conditions must be true
  def category(age) when age >= 20 and age < 65, do: "adult"
  # => Guards are evaluated at dispatch time (before function body)
  def category(age) when age >= 65, do: "senior"
  # => Last specific clause before catch-all

  # Multiple guards with `or`
  def weekday(day) when day == :saturday or day == :sunday, do: "weekend"
  # => or: either condition matches
  # => == checks value equality
  def weekday(_day), do: "weekday"
  # => Catch-all for all other days

  # Guard functions (limited set allowed for purity)
  def valid_user(name, age) when is_binary(name) and byte_size(name) > 0 and age >= 18 do
    # => byte_size/1: allowed guard function (measures binary bytes)
    # => Three conditions ANDed: type check, non-empty, age check
    {:ok, %{name: name, age: age}}
    # => Returns tagged tuple on success
  end
  def valid_user(_name, _age), do: {:error, "invalid user"}
  # => Catch-all for validation failures

  # Pattern matching with guards (combine both techniques)
  def process_response({:ok, status, body}) when status >= 200 and status < 300 do
    # => Pattern: {:ok, status, body} destructures tuple
    # => Guard: checks status is 2xx (success range)
    {:success, body}
    # => Returns normalized :success tuple
  end
  def process_response({:ok, status, _body}) when status >= 400 do
    # => Pattern matches :ok tuple but guards on 4xx+ status
    # => _ prefix: body ignored (not used in function)
    {:error, "client error: #{status}"}
    # => Returns error with status code
  end
  def process_response({:error, reason}) do
    # => Pattern matches :error tuple (no guard needed)
    {:error, "request failed: #{reason}"}
    # => Wraps reason in descriptive message
  end

  # Allowed guard functions (restricted for performance & safety):
  # Type checks: is_atom, is_binary, is_boolean, is_float, is_integer, is_list, is_map, is_tuple
  # Comparisons: ==, !=, ===, !==, <, >, <=, >=
  # Boolean: and, or, not
  # Arithmetic: +, -, *, /
  # Others: abs, div, rem, length, byte_size, tuple_size, elem, hd, tl
  # => Restriction: guards must be pure (no side effects, no IO, no exceptions)
  # => Reason: guards execute during pattern matching (before stack allocation)

  # Custom guard-safe functions (using defguard macro)
  defguard is_adult(age) when is_integer(age) and age >= 18
  # => defguard: defines reusable guard expression
  # => Expands inline at compile time (zero runtime overhead)
  # => Must only use other guard-safe operations

  def can_vote(age) when is_adult(age), do: true
  # => Uses custom guard like built-in
  # => Expands to: when is_integer(age) and age >= 18
  def can_vote(_age), do: false
  # => Catch-all for non-adults
end

# Type dispatching
Guards.type_check(42)
# => Tries clause 1: is_integer(42) → true → "integer: 42"
Guards.type_check(3.14)
# => Tries clause 1: is_integer(3.14) → false
# => Tries clause 2: is_float(3.14) → true → "float: 3.14"
Guards.type_check("hello")
# => Tries clauses 1-2: both fail
# => Tries clause 3: is_binary("hello") → true → "string: hello"
Guards.type_check(:atom)
# => Tries clauses 1-3: all fail
# => Tries clause 4: is_atom(:atom) → true → "atom: :atom"

# Value range dispatching
Guards.category(10)
# => Guard: 10 < 13 → true → "child"
Guards.category(15)
# => Guard: 15 < 13 → false
# => Guard: 15 >= 13 and 15 < 20 → true → "teen"
Guards.category(30)
# => Guards: tries clauses 1-2, both fail
# => Guard: 30 >= 20 and 30 < 65 → true → "adult"
Guards.category(70)
# => Guards: tries clauses 1-3, all fail
# => Guard: 70 >= 65 → true → "senior"

# OR guards
Guards.weekday(:saturday)
# => Guard: :saturday == :saturday or ... → true (short-circuits) → "weekend"
Guards.weekday(:monday)
# => Guard: :monday == :saturday → false
# => Guard: :monday == :sunday → false → or fails
# => Tries clause 2: always matches → "weekday"

# Complex validation
Guards.valid_user("Alice", 25)
# => Guard: is_binary("Alice") → true
# => Guard: byte_size("Alice") > 0 → 5 > 0 → true
# => Guard: 25 >= 18 → true
# => All conditions true → {:ok, %{age: 25, name: "Alice"}}
Guards.valid_user("", 25)
# => Guard: is_binary("") → true
# => Guard: byte_size("") > 0 → 0 > 0 → false → guard fails
# => Tries clause 2 → {:error, "invalid user"}
Guards.valid_user("Bob", 15)
# => Guards: is_binary("Bob") → true, byte_size("Bob") > 0 → true
# => Guard: 15 >= 18 → false → guard fails
# => Tries clause 2 → {:error, "invalid user"}

# HTTP response handling
Guards.process_response({:ok, 200, "Success"})
# => Pattern: {:ok, 200, "Success"} matches {:ok, status, body}
# => Guard: 200 >= 200 and 200 < 300 → true
# => {:success, "Success"}
Guards.process_response({:ok, 404, "Not Found"})
# => Pattern: {:ok, 404, "Not Found"} matches clause 1
# => Guard: 404 >= 200 and 404 < 300 → false → clause 1 fails
# => Tries clause 2: {:ok, 404, "Not Found"} matches {:ok, status, _body}
# => Guard: 404 >= 400 → true → {:error, "client error: 404"}
Guards.process_response({:error, :timeout})
# => Pattern: {:error, :timeout} doesn't match clause 1 or 2
# => Tries clause 3: matches {:error, reason} → {:error, "request failed: timeout"}

# Custom guard usage
Guards.can_vote(25)
# => Guard: is_adult(25) expands to is_integer(25) and 25 >= 18
# => Both true → true
Guards.can_vote(16)
# => Guard: is_integer(16) and 16 >= 18 → true and false → false
# => Tries clause 2 → false

Key Takeaway: Guards add type and value constraints to pattern matching. Only a limited set of functions is allowed in guards to ensure they remain side-effect free and fast.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 32: Pattern Matching in Function Heads

Multi-clause functions use pattern matching in function heads to elegantly handle different input shapes. Clauses are tried in order from top to bottom until one matches.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Input["Function Call:<br/>handle({:ok, data})"] --> Clause1{" Clause 1:<br/>{:error, _}?"}
    Clause1 -->|No| Clause2{"Clause 2:<br/>{:ok, data}?"}
    Clause2 -->|Yes| Execute["Execute:<br/>Process data"]
    Clause1 -->|Yes| Error["Execute:<br/>Handle error"]

    style Input fill:#0173B2,color:#fff
    style Clause1 fill:#DE8F05,color:#fff
    style Clause2 fill:#DE8F05,color:#fff
    style Execute fill:#029E73,color:#fff
    style Error fill:#CC78BC,color:#fff

Code:

defmodule FunctionMatching do
  # Order matters! Specific cases before general cases
  def handle_result({:ok, value}), do: "Success: #{value}"
  # => Pattern: {:ok, value} matches 2-element tuple with :ok atom
  # => value extracts second element
  def handle_result({:error, reason}), do: "Error: #{reason}"
  # => Different pattern: {:error, _} would match any error
  def handle_result(_), do: "Unknown result"
  # => Catch-all: matches anything not matched above
  # => WARNING: Must be last clause (matches everything)

  # Pattern matching with destructuring (tagged tuples)
  def greet({:user, name}), do: "Hello, #{name}!"
  # => Matches {:user, "Alice"} → extracts "Alice" into name
  def greet({:admin, name}), do: "Welcome back, Admin #{name}!"
  # => Different first element → different clause
  def greet({:guest}), do: "Welcome, guest!"
  # => Single-element tuple pattern (guest has no name)

  # List pattern matching (recursive)
  def sum([]), do: 0
  # => Base case: empty list returns 0
  # => Recursion terminates here
  def sum([head | tail]), do: head + sum(tail)
  # => Pattern: [head | tail] splits list into first element + rest
  # => Recursive case: add head to sum of tail
  # => Example: [1,2,3] → 1 + sum([2,3]) → 1 + 2 + sum([3]) → 1 + 2 + 3 + sum([]) → 6

  # Map pattern matching (structural matching)
  def user_summary(%{name: name, age: age}) when age >= 18 do
    # => Pattern: %{name: name, age: age} extracts specific keys
    # => Map can have other keys (ignored)
    # => Guard: age >= 18 adds constraint after pattern match
    "#{name} is an adult (#{age} years old)"
  end
  def user_summary(%{name: name, age: age}) do
    # => Same pattern, different guard
    # => Clause order matters: guard checked first-to-last
    "#{name} is a minor (#{age} years old)"
  end

  # Multiple pattern matches with guards (value classification)
  def classify_number(n) when n < 0, do: :negative
  # => Guard only (no pattern destructuring needed)
  def classify_number(0), do: :zero
  # => Exact value pattern (no guard needed)
  def classify_number(n) when n > 0 and n < 100, do: :small_positive
  # => Compound guard: both conditions must be true
  def classify_number(n) when n >= 100, do: :large_positive
  # => Last specific clause (implicitly covers n >= 100)

  # Complex nested patterns (multi-level destructuring)
  def process_response({:ok, %{status: 200, body: body}}) do
    # => Nested pattern: tuple contains map
    # => Matches: {:ok, %{status: 200, body: "anything"}}
    # => Extracts: body value
    {:success, body}
  end
  def process_response({:ok, %{status: status, body: _}}) when status >= 400 do
    # => Pattern: tuple + map destructuring
    # => _ ignores body (not used)
    # => Guard: status >= 400 further constrains
    {:client_error, status}
  end
  def process_response({:error, %{reason: reason}}) do
    # => Different tuple tag: :error instead of :ok
    # => Still uses map pattern for nested data
    {:failed, reason}
  end

  # Default arguments with pattern matching (multi-clause + defaults)
  def send_message(user, message, opts \\ [])
  # => Function head: declares default value opts = []
  # => Actual implementations below (pattern match on opts)
  def send_message(%{email: email}, message, priority: :high) do
    # => Pattern: opts must be [priority: :high] (keyword list)
    # => user must be map with :email key
    "Urgent email to #{email}: #{message}"
  end
  def send_message(%{email: email}, message, _opts) do
    # => Catch-all for opts (any value including [])
    # => Matches when first clause doesn't
    "Email to #{email}: #{message}"
  end
end

# Result tuple handling
FunctionMatching.handle_result({:ok, 42})
# => Tries clause 1: {:ok, 42} matches {:ok, value} → "Success: 42"
FunctionMatching.handle_result({:error, "not found"})
# => Clause 1 fails (not :ok)
# => Clause 2: {:error, "not found"} matches {:error, reason} → "Error: not found"
FunctionMatching.handle_result(:unknown)
# => Clauses 1-2 fail (not tuple)
# => Clause 3: _ matches anything → "Unknown result"

# Tagged tuple dispatching
FunctionMatching.greet({:user, "Alice"})
# => Clause 1: {:user, "Alice"} matches {:user, name} → "Hello, Alice!"
FunctionMatching.greet({:admin, "Bob"})
# => Clause 1 fails (:admin ≠ :user)
# => Clause 2: {:admin, "Bob"} matches {:admin, name} → "Welcome back, Admin Bob!"
FunctionMatching.greet({:guest})
# => Clauses 1-2 fail (wrong pattern)
# => Clause 3: {:guest} matches → "Welcome, guest!"

# Recursive list processing
FunctionMatching.sum([1, 2, 3, 4])
# => [1 | [2,3,4]] → 1 + sum([2,3,4])
# => [2 | [3,4]] → 2 + sum([3,4])
# => [3 | [4]] → 3 + sum([4])
# => [4 | []] → 4 + sum([])
# => [] → 0
# => Stack unwinds: 4 + 0 = 4, 3 + 4 = 7, 2 + 7 = 9, 1 + 9 = 10
FunctionMatching.sum([])
# => Clause 1: [] matches → 0 (base case)

# Map pattern matching with guards
FunctionMatching.user_summary(%{name: "Alice", age: 25})
# => Pattern: %{name: "Alice", age: 25} matches %{name: name, age: age}
# => Extracts: name = "Alice", age = 25
# => Guard: 25 >= 18 → true → "Alice is an adult (25 years old)"
FunctionMatching.user_summary(%{name: "Bob", age: 16})
# => Pattern matches clause 1
# => Guard: 16 >= 18 → false → clause 1 fails
# => Clause 2: same pattern, no guard → matches → "Bob is a minor (16 years old)"

# Value classification with guards
FunctionMatching.classify_number(-5)
# => Guard: -5 < 0 → true → :negative
FunctionMatching.classify_number(0)
# => Clause 1 guard fails
# => Pattern: 0 matches exactly → :zero
FunctionMatching.classify_number(50)
# => Clauses 1-2 fail
# => Guard: 50 > 0 and 50 < 100 → true → :small_positive
FunctionMatching.classify_number(200)
# => Clauses 1-3 fail
# => Guard: 200 >= 100 → true → :large_positive

# Nested pattern matching
FunctionMatching.process_response({:ok, %{status: 200, body: "OK"}})
# => Pattern: {:ok, %{status: 200, body: body}} matches exactly
# => Extracts: body = "OK" → {:success, "OK"}
FunctionMatching.process_response({:ok, %{status: 404, body: "Not Found"}})
# => Clause 1: status 404 ≠ 200 → fails
# => Clause 2: pattern matches, guard 404 >= 400 → true → {:client_error, 404}
FunctionMatching.process_response({:error, %{reason: :timeout}})
# => Clauses 1-2: {:error, ...} doesn't match {:ok, ...}
# => Clause 3: {:error, %{reason: :timeout}} matches → {:failed, :timeout}

# Default arguments with pattern matching
FunctionMatching.send_message(%{email: "a@example.com"}, "Hello", priority: :high)
# => opts = [priority: :high] (provided)
# => Clause 1: pattern [priority: :high] matches → "Urgent email..."
FunctionMatching.send_message(%{email: "b@example.com"}, "Hi", [])
# => opts = [] (provided empty list)
# => Clause 1: [] doesn't match [priority: :high]
# => Clause 2: _opts matches [] → "Email..."

Key Takeaway: Pattern matching in function heads enables elegant multi-clause logic. Place specific patterns before general ones, and combine with guards for precise control flow.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 33: With Expression (Happy Path)

The with expression chains pattern matches, short-circuiting on the first mismatch. It’s ideal for “happy path” coding where you expect success and want to handle errors at the end.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Start["with"] --> Match1["Step 1:<br/>{:ok, user} <- get_user()"]
    Match1 -->|Match| Match2["Step 2:<br/>{:ok, account} <- get_account()"]
    Match2 -->|Match| Match3["Step 3:<br/>{:ok, balance} <- get_balance()"]
    Match3 -->|Match| Do["do:<br/>Process happy path"]

    Match1 -->|No Match| Else["else:<br/>Handle error"]
    Match2 -->|No Match| Else
    Match3 -->|No Match| Else

    style Start fill:#0173B2,color:#fff
    style Match1 fill:#DE8F05,color:#fff
    style Match2 fill:#DE8F05,color:#fff
    style Match3 fill:#DE8F05,color:#fff
    style Do fill:#029E73,color:#fff
    style Else fill:#CC78BC,color:#fff

Code:

defmodule WithExamples do           # => Defines module for with expression examples
  # Simulate API functions (return tagged tuples)
  def fetch_user(id) do             # => Simulates database/API user fetch
                                     # => Returns tagged tuple {:ok, user} or {:error, reason}
    case id do                       # => Pattern matches on user id
      1 -> {:ok, %{id: 1, name: "Alice", account_id: 101}}
                                     # => Success: returns {:ok, user_map}
                                     # => User 1 linked to account 101
      2 -> {:ok, %{id: 2, name: "Bob", account_id: 102}}
                                     # => User 2 linked to account 102
      _ -> {:error, :user_not_found}  # => Failure: returns {:error, atom}
                                     # => Any other id returns error
    end                              # => Ends case expression
  end                                # => Ends fetch_user function

  def fetch_account(account_id) do  # => Simulates account data fetch
                                     # => Takes account_id from user record
    case account_id do               # => Pattern matches on account_id
      101 -> {:ok, %{id: 101, balance: 1000}}
                                     # => Account 101 has balance 1000
      102 -> {:ok, %{id: 102, balance: 500}}
                                     # => Account 102 has balance 500
      _ -> {:error, :account_not_found}
                                     # => Unknown account returns error
    end
  end

  def fetch_transactions(account_id) do  # => Simulates transaction history fetch
                                     # => Returns list of transaction maps
    case account_id do
      101 -> {:ok, [%{amount: 100}, %{amount: -50}]}
                                     # => Returns list of transaction maps
                                     # => Account 101 has 2 transactions
      102 -> {:ok, [%{amount: 200}]}  # => Account 102 has 1 transaction
      _ -> {:error, :transactions_not_found}
                                     # => Unknown account returns error
    end
  end

  # ❌ WITHOUT `with` - nested case statements (pyramid of doom)
  def get_user_summary_nested(user_id) do  # => Traditional nested approach
                                     # => Demonstrates problem with solves
    case fetch_user(user_id) do      # => First API call
                                     # => Level 1 nesting
      {:ok, user} ->                 # => User fetch succeeded
                                     # => Extracts user from {:ok, user} tuple
        case fetch_account(user.account_id) do  # => Second API call (nested)
                                     # => Level 2 nesting
                                     # => Uses user.account_id from level 1
          {:ok, account} ->          # => Account fetch succeeded
                                     # => Extracts account from tuple
            case fetch_transactions(account.id) do  # => Third API call (deeply nested)
                                     # => Level 3 nesting (hard to read!)
                                     # => Uses account.id from level 2
              {:ok, transactions} -> # => All three fetches succeeded
                                     # => Extracts transactions from tuple
                {:ok, %{user: user, account: account, transactions: transactions}}
                                     # => Builds success result map
                                     # => Returns all three fetched values
              {:error, reason} ->    # => Transaction fetch failed
                {:error, reason}     # => Must repeat error handling at each level
            end
          {:error, reason} ->        # => Account fetch failed
            {:error, reason}         # => Duplicated error handling
        end
      {:ok, reason} ->        # => User fetch failed
        {:error, reason}             # => Error handling repeated 3 times!
                                     # => Same pattern at every nesting level
    end                              # => Ends deeply nested case expression
  end                                # => Ends nested function

  # ✅ WITH `with` - clean happy path (linear, readable)
  def get_user_summary(user_id) do  # => Clean version using with expression
                                     # => Same logic, much more readable
    with {:ok, user} <- fetch_user(user_id),
                                     # => Step 1: pattern match {:ok, user}
                                     # => Left side is pattern, right side is expression
                                     # => If matches: continue to step 2
                                     # => If doesn't match: jump to else block
         {:ok, account} <- fetch_account(user.account_id),
                                     # => Step 2: uses user from step 1
                                     # => user binding available from previous step
                                     # => Chain continues only if pattern matches
         {:ok, transactions} <- fetch_transactions(account.id) do
                                     # => Step 3: uses account from step 2
                                     # => account binding available from step 2
                                     # => All patterns matched: execute do block
      # Happy path - all matches succeeded
      {:ok, %{                       # => Builds success result
        user: user.name,             # => user binding from step 1
                                     # => Extracts name field from user map
        balance: account.balance,    # => account binding from step 2
                                     # => Extracts balance field
        transaction_count: length(transactions)
                                     # => transactions binding from step 3
                                     # => Counts number of transactions
      }}                             # => Returns {:ok, summary_map}
    else                             # => Handles any pattern mismatch
                                     # => First mismatch jumps here (short-circuit)
      {:error, :user_not_found} -> {:error, "User not found"}
                                     # => Pattern match error from step 1
                                     # => Converts atom to user-friendly message
      {:error, :account_not_found} -> {:error, "Account not found"}
                                     # => Pattern match error from step 2
      {:error, :transactions_not_found} -> {:error, "Transactions not found"}
                                     # => Pattern match error from step 3
                                     # => Consolidated error handling in one place!
    end                              # => Ends with expression
  end                                # => Ends get_user_summary function

  # `with` can match any pattern (not just :ok/:error)
  def complex_calculation(x) do      # => Demonstrates with for calculation chains
                                     # => Shows pattern matching flexibility
    with {:ok, doubled} <- {:ok, x * 2},
                                     # => x = 5 → {:ok, 10}
                                     # => Right side evaluates, left pattern matches
                                     # => Matches pattern, doubled = 10
         {:ok, incremented} <- {:ok, doubled + 1},
                                     # => {:ok, 11}, incremented = 11
                                     # => Uses doubled from previous step
         {:ok, squared} <- {:ok, incremented * incremented} do
                                     # => {:ok, 121}, squared = 121
                                     # => Uses incremented from previous step
      {:ok, squared}                 # => Returns {:ok, 121}
                                     # => All steps succeeded, return final value
    else                             # => Handles pattern mismatch (won't happen here)
      _ -> {:error, "calculation failed"}
                                     # => Catch-all for any non-matching pattern
                                     # => Would execute if any step failed
    end                              # => Ends with expression
  end                                # => Ends complex_calculation function

  # Boolean guards in `with` (Elixir 1.3+)
  def process_number(x) when is_integer(x) do  # => Function guard ensures integer
                                     # => with can validate with boolean expressions
    with true <- x > 0,              # => Matches true or jumps to else
                                     # => Pattern: true <- boolean_expression
                                     # => First validation: x must be positive
         true <- x < 100 do          # => Second validation: x must be < 100
                                     # => Both must match true to proceed
      {:ok, "Valid number: #{x}"}    # => Both guards passed
                                     # => String interpolation with validated x
    else                             # => Handles guard failures
      false -> {:error, "Number out of range"}
                                     # => Either guard failed (returned false)
                                     # => Both failures use same pattern
                                     # => Unified error message for range violations
    end                              # => Ends with expression
  end                                # => Ends process_number function
end                                  # => Ends WithExamples module

# Happy path - all steps succeed
WithExamples.get_user_summary(1)  # => Calls with user_id 1
# => Step 1: fetch_user(1) → {:ok, %{id: 1, name: "Alice", account_id: 101}}
# => Pattern {:ok, user} matches, user bound to map
# => Step 2: fetch_account(101) → {:ok, %{id: 101, balance: 1000}}
# => Pattern {:ok, account} matches, account bound to map
# => Step 3: fetch_transactions(101) → {:ok, [...]}
# => Pattern {:ok, transactions} matches, all steps succeeded
# => do block executes → {:ok, %{balance: 1000, transaction_count: 2, user: "Alice"}}

WithExamples.get_user_summary(2)  # => Calls with user_id 2
# => User Bob, account 102, balance 500, 1 transaction
# => All three with steps succeed
# => {:ok, %{balance: 500, transaction_count: 1, user: "Bob"}}

# Failure path - short-circuit at step 1
WithExamples.get_user_summary(999)  # => Calls with invalid user_id
# => Step 1: fetch_user(999) → {:error, :user_not_found}
# => Pattern {:ok, user} doesn't match → jumps to else
# => Steps 2 and 3 never execute (short-circuit)
# => else block: {:error, :user_not_found} matches → {:error, "User not found"}

# Calculation chain
WithExamples.complex_calculation(5)  # => Demonstrates with for calculations
# => Step 1: {:ok, 5 * 2} = {:ok, 10}
# => Pattern matches, doubled = 10
# => Step 2: {:ok, 10 + 1} = {:ok, 11}
# => Pattern matches, incremented = 11
# => Step 3: {:ok, 11 * 11} = {:ok, 121}
# => Pattern matches, squared = 121
# => {:ok, 121}

# Boolean guard validation
WithExamples.process_number(50)  # => Tests boolean guards in with
# => Guard 1: 50 > 0 → true (matches)
# => First pattern true <- true succeeds
# => Guard 2: 50 < 100 → true (matches)
# => Second pattern true <- true succeeds
# => {:ok, "Valid number: 50"}

WithExamples.process_number(150)
# => Guard 1: 150 > 0 → true
# => Guard 2: 150 < 100 → false (doesn't match true)
# => else: false matched → {:error, "Number out of range"}

WithExamples.process_number(-10)
# => Guard 1: -10 > 0 → false (short-circuits)
# => else: false matched → {:error, "Number out of range"}

Key Takeaway: with chains pattern matches and short-circuits on the first mismatch. Use it for happy path coding where you expect success, with error handling consolidated in the else block.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 34: Structs

Structs are extensions of maps with compile-time guarantees and default values. They enforce a predefined set of keys, enabling clearer data modeling and better error messages.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Map["Regular Map<br/>%{any: keys, ...}"] --> Struct["Struct<br/>%User{name: ..., age: ...}"]
    Struct --> Tag["Special __struct__: User key"]
    Struct --> Keys["Enforced keys:<br/>name, age"]
    Struct --> Defaults["Default values:<br/>active: true"]

    style Map fill:#0173B2,color:#fff
    style Struct fill:#DE8F05,color:#fff
    style Tag fill:#029E73,color:#fff
    style Keys fill:#CC78BC,color:#fff
    style Defaults fill:#CA9161,color:#fff

Struct Features

Tagged Maps: Structs are maps with special __struct__ key (module name) Default Values: Define defaults for fields (e.g., active: true) Enforced Keys: @enforce_keys requires fields at creation (compile-time check) Pattern Matching: Match on struct type for type-safe function dispatch Immutability: Updates create new structs (original unchanged)

Code:

defmodule User do
  # Define struct with default values
  defstruct name: nil, age: nil, email: nil, active: true
  # => All fields optional with defaults
  # => active defaults to true (others nil)

  # Alternative: enforced keys (compile-time check)
  # @enforce_keys [:name, :age]
  # => Compile error if keys not provided at creation
  # defstruct [:name, :age, email: nil, active: true]
end

defmodule Account do
  @enforce_keys [:id, :balance]
  # => MUST provide :id and :balance at creation
  # => Raises ArgumentError if missing
  defstruct [:id, :balance, status: :active, transactions: []]
  # => :id, :balance required; status/:transactions have defaults
end

# Create struct with all fields
user = %User{name: "Alice", age: 30, email: "alice@example.com"}
# => Syntax: %ModuleName{key: value, ...}
# => active uses default (true)
# => %User{name: "Alice", age: 30, email: "alice@example.com", active: true}

# Create struct with partial fields
user_partial = %User{name: "Bob", age: 25}
# => email: nil (default), active: true (default)

# Access struct fields (dot notation)
user.name
# => "Alice"
user.age
# => 30
user.active
# => true (default value)

# Update struct (immutable, creates new struct)
updated_user = %{user | age: 31, email: "alice.new@example.com"}
# => Syntax: %{struct | field: new_value, ...}
# => Returns NEW struct (original unchanged)
user.age
# => 30 (original unchanged - immutability)

# Struct is a tagged map
user.__struct__
# => User (module name stored in __struct__ field)
is_map(user)
# => true (structs ARE maps)
Map.keys(user)
# => [:__struct__, :active, :age, :email, :name]

# Pattern matching on structs
%User{name: name, age: age} = user
# => Destructures struct fields
# => Only matches User structs (not other types)
name
# => "Alice"
age
# => 30

# Pattern matching in function heads (type safety)
def greet_user(%User{name: name}), do: "Hello, #{name}!"
# => Only accepts User structs (not Account or plain maps)
# => Type-safe dispatch
greet_user(user)
# => "Hello, Alice!"
# greet_user(%{name: "Bob"}) would raise FunctionClauseError

# Enforced keys example
account = %Account{id: 1, balance: 1000}
# => MUST provide :id and :balance (@enforce_keys)
# => status and transactions use defaults
# => %Account{id: 1, balance: 1000, status: :active, transactions: []}

# account_invalid = %Account{id: 1}
# => ** (ArgumentError) missing required keys: [:balance]

Key Takeaway: Structs are tagged maps with enforced keys and default values. They provide compile-time guarantees and clearer domain modeling compared to plain maps.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 35: Streams (Lazy Enumeration)

Streams are lazy enumerables that build a recipe for computation without executing it immediately. They enable efficient processing of large or infinite datasets by composing transformations.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Eager["Enum (Eager)<br/>[1,2,3,4,5]"] --> Map1["map: [2,4,6,8,10]<br/>EXECUTES"]
    Map1 --> Filter1["filter: [2,4,6,8,10]<br/>EXECUTES"]
    Filter1 --> Take1["take 2: [2,4]<br/>EXECUTES"]

    Lazy["Stream (Lazy)<br/>[1,2,3,4,5]"] --> Map2["map: recipe<br/>NO EXECUTION"]
    Map2 --> Filter2["filter: recipe<br/>NO EXECUTION"]
    Filter2 --> Take2["take 2: recipe<br/>NO EXECUTION"]
    Take2 --> Realize["Enum.to_list: [2,4]<br/>EXECUTE ONCE"]

    style Eager fill:#0173B2,color:#fff
    style Lazy fill:#0173B2,color:#fff
    style Map1 fill:#CC78BC,color:#fff
    style Filter1 fill:#CC78BC,color:#fff
    style Take1 fill:#CC78BC,color:#fff
    style Map2 fill:#DE8F05,color:#fff
    style Filter2 fill:#DE8F05,color:#fff
    style Take2 fill:#DE8F05,color:#fff
    style Realize fill:#029E73,color:#fff

Code:

numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# Eager evaluation (Enum) - immediate execution
eager_result = numbers
               |> Enum.map(fn x -> x * 2 end)
               # => Pass 1: builds [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
               # => Processes all 10 elements immediately
               |> Enum.filter(fn x -> rem(x, 4) == 0 end)
               # => Pass 2: builds [4, 8, 12, 16, 20]
               # => Processes all 5 remaining elements
               |> Enum.take(2)
               # => Pass 3: takes first 2 → [4, 8]
               # => Already processed all 10 elements (wasteful!)

# Lazy evaluation (Stream) - deferred execution
lazy_result = numbers
              |> Stream.map(fn x -> x * 2 end)
              # => Returns #Stream<...> (NOT a list)
              # => No execution yet, just builds recipe
              |> Stream.filter(fn x -> rem(x, 4) == 0 end)
              # => Returns #Stream<...> (still no execution)
              # => Composes filter into recipe
              |> Enum.take(2)
              # => TRIGGERS execution: processes elements one-by-one
              # => Stops after finding 2 matches
              # => Only processes: 1→2 (no), 2→4 (YES), 3→6 (no), 4→8 (YES), STOP
              # => [4, 8] (processed 4 elements, not 10!)

# Infinite streams (impossible with eager evaluation)
infinite_numbers = Stream.iterate(1, fn x -> x + 1 end)
# => Infinite sequence: 1, 2, 3, 4, 5, ...
# => Returns #Stream<...> (no execution yet)
# => Would never terminate if eager!

first_evens = infinite_numbers
              |> Stream.filter(fn x -> rem(x, 2) == 0 end)
              # => Lazy filter (no execution)
              |> Enum.take(10)
              # => Takes first 10 even numbers
              # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
              # => Stops after 10 matches (doesn't process infinite stream!)

# Stream.cycle - repeats list infinitely
Stream.cycle([1, 2, 3]) |> Enum.take(7)
# => Cycles through [1,2,3] infinitely
# => Takes first 7: [1, 2, 3, 1, 2, 3, 1]

# Stream.unfold - generates values from state
fibonacci = Stream.unfold({0, 1}, fn {a, b} -> {a, {b, a + b}} end)
# => unfold(initial_state, next_fn)
# => next_fn returns {value, new_state}
# => {0, 1} → emit 0, next state {1, 1}
# => {1, 1} → emit 1, next state {1, 2}
# => {1, 2} → emit 1, next state {2, 3}
# => {2, 3} → emit 2, next state {3, 5}
# => Infinite Fibonacci sequence
Enum.take(fibonacci, 10)
# => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

# Performance comparison
defmodule Performance do
  def eager_pipeline(n) do
    1..n
    |> Enum.map(fn x -> x * 2 end)
    # => Builds entire intermediate list (n elements)
    |> Enum.filter(fn x -> rem(x, 3) == 0 end)
    # => Builds another intermediate list (~n/3 elements)
    |> Enum.take(100)
    # => Takes 100, discards rest
    # => Wastes memory and CPU for large n
  end

  def lazy_pipeline(n) do
    1..n
    |> Stream.map(fn x -> x * 2 end)
    # => No intermediate list (lazy)
    |> Stream.filter(fn x -> rem(x, 3) == 0 end)
    # => Still lazy (just composing)
    |> Enum.take(100)
    # => Processes ONLY until 100 matches found
    # => For n=1_000_000: processes ~300 elements, not 1 million!
  end
end

# Performance.eager_pipeline(1_000_000)
# => Processes 1 million elements, builds 2 intermediate lists
# Performance.lazy_pipeline(1_000_000)
# => Processes ~300 elements, 0 intermediate lists

# Stream.resource - manage external resources (files, sockets, etc.)
stream_resource = Stream.resource(
  fn -> {:ok, "initial state"} end,
  # => Start function: called once to initialize
  # => Returns initial accumulator state
  fn state -> {[state], "next state"} end,
  # => Next function: called repeatedly
  # => Returns {values_to_emit, new_state}
  # => Can return {:halt, state} to stop
  fn _state -> :ok end
  # => After function: cleanup (called when stream ends)
  # => Close files, release resources, etc.
)
Enum.take(stream_resource, 3)
# => Calls start → {:ok, "initial state"}
# => Calls next("initial state") → {["initial state"], "next state"}
# => Emits "initial state"
# => Calls next("next state") → {["next state"], "next state"}
# => Emits "next state" (2 times)
# => Calls after("next state") → :ok (cleanup)
# => ["initial state", "next state", "next state"]

Key Takeaway: Streams enable lazy evaluation—building a recipe without executing it. Use streams for large datasets, infinite sequences, or when you want to compose transformations efficiently.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 36: MapSet for Uniqueness

MapSets are unordered collections of unique values. They provide efficient membership testing and set operations (union, intersection, difference). Use them when uniqueness matters and order doesn’t.

Code:

# Create MapSet from list (automatic deduplication)
set1 = MapSet.new([1, 2, 3, 3, 4, 4, 5])
# => MapSet.new/1 removes duplicates automatically
# => #MapSet<[1, 2, 3, 4, 5]>
# => Unordered: order not guaranteed

# Create MapSet from range
set_range = MapSet.new(1..10)
# => Converts range to set
# => #MapSet<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]>

# Add element (immutable, returns new set)
set2 = MapSet.put(set1, 6)
# => Adds 6 to set (new set returned)
# => #MapSet<[1, 2, 3, 4, 5, 6]>
set1
# => #MapSet<[1, 2, 3, 4, 5]> (original unchanged!)
# => Immutability: set2 is separate copy

# Add duplicate (no effect)
set3 = MapSet.put(set1, 3)
# => 3 already exists in set
# => Returns set unchanged
# => #MapSet<[1, 2, 3, 4, 5]> (same as set1)

# Remove element
set4 = MapSet.delete(set1, 3)
# => Removes 3 from set
# => #MapSet<[1, 2, 4, 5]>

# Membership testing (O(log n))
MapSet.member?(set1, 3)
# => Checks if 3 is in set
# => true (efficient lookup)
MapSet.member?(set1, 10)
# => Checks if 10 is in set
# => false (not present)

# Set size
MapSet.size(set1)
# => Counts unique elements
# => 5

# Set operations
setA = MapSet.new([1, 2, 3])
# => Set A: {1, 2, 3}
setB = MapSet.new([3, 4, 5])
# => Set B: {3, 4, 5}

# Union (all unique elements from both sets)
MapSet.union(setA, setB)
# => A ∪ B: elements in A OR B
# => #MapSet<[1, 2, 3, 4, 5]>

# Intersection (common elements)
MapSet.intersection(setA, setB)
# => A ∩ B: elements in A AND B
# => #MapSet<[3]> (only 3 is common)

# Difference (elements in A but not in B)
MapSet.difference(setA, setB)
# => A \ B: elements in A but NOT in B
# => #MapSet<[1, 2]> (3 is removed)
MapSet.difference(setB, setA)
# => B \ A: elements in B but NOT in A
# => #MapSet<[4, 5]> (3 is removed)
# => Difference is NOT commutative!

# Subset and superset
setX = MapSet.new([1, 2])
# => Set X: {1, 2}
setY = MapSet.new([1, 2, 3, 4])
# => Set Y: {1, 2, 3, 4}
MapSet.subset?(setX, setY)
# => Is X ⊆ Y? (all elements of X in Y?)
# => true (1 and 2 are in Y)
MapSet.subset?(setY, setX)
# => Is Y ⊆ X?
# => false (3 and 4 not in X)

# Disjoint sets (no common elements)
MapSet.disjoint?(setA, MapSet.new([6, 7]))
# => Do A and {6, 7} have any common elements?
# => true (no overlap)
# => Disjoint: A ∩ {6,7} = ∅

# Convert to list (order not guaranteed)
MapSet.to_list(set1)
# => Converts set to list
# => [1, 2, 3, 4, 5] (order may vary)
# => Sets are unordered!

# Practical example: unique tags from posts
posts = [
  %{id: 1, tags: ["elixir", "functional", "programming"]},
  %{id: 2, tags: ["elixir", "otp", "concurrency"]},
  %{id: 3, tags: ["functional", "fp", "programming"]}
]

# Extract all unique tags
all_tags = posts
           |> Enum.flat_map(fn post -> post.tags end)
           # => Flattens: ["elixir", "functional", "programming", "elixir", "otp", ...]
           # => With duplicates
           |> MapSet.new()
           # => Deduplicates: #MapSet<["elixir", "functional", "programming", "otp", "concurrency", "fp"]>
# => Automatic uniqueness!

# Find common tags between posts
post1_tags = MapSet.new(["elixir", "functional"])
post2_tags = MapSet.new(["elixir", "otp"])
MapSet.intersection(post1_tags, post2_tags)
# => Common tags: #MapSet<["elixir"]>
# => Useful for finding related content

Key Takeaway: MapSets provide O(log n) membership testing and automatic deduplication. Use them for unique collections where order doesn’t matter and set operations (union, intersection, difference) are needed.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 37: Module Attributes

Module attributes are compile-time constants defined with @. They’re commonly used for documentation (@moduledoc, @doc), compile-time configuration, and storing values computed during compilation.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    CompileTime["Compile Time"] --> Attrs["Module Attributes Evaluated"]
    Attrs --> Version["@version = '1.0.0'"]
    Attrs --> Timeout["@default_timeout = 5000"]
    Attrs --> Languages["@languages = [...list...]"]
    Languages --> Count["@language_count = 3<br/>(computed: length list)"]

    Runtime["Runtime"] --> Inline["Attributes inlined"]
    Inline --> VersionFunc["version() -> '1.0.0'<br/>(zero-cost constant)"]
    Inline --> TimeoutFunc["wait(5000)<br/>(default from @default_timeout)"]
    Inline --> CountFunc["language_count() -> 3<br/>(pre-computed)"]

    Reserved["Reserved Attributes"] --> ModuleDoc["@moduledoc<br/>(documentation)"]
    Reserved --> Doc["@doc<br/>(function docs)"]
    Reserved --> Behaviour["@behaviour<br/>(callback verification)"]
    Reserved --> Impl["@impl<br/>(marks callbacks)"]

    style CompileTime fill:#0173B2,color:#fff
    style Attrs fill:#DE8F05,color:#fff
    style Version fill:#029E73,color:#fff
    style Timeout fill:#029E73,color:#fff
    style Languages fill:#029E73,color:#fff
    style Count fill:#029E73,color:#fff
    style Runtime fill:#CC78BC,color:#fff
    style Inline fill:#DE8F05,color:#fff
    style Reserved fill:#CA9161,color:#fff
    style ModuleDoc fill:#CA9161,color:#fff
    style Doc fill:#CA9161,color:#fff
    style Behaviour fill:#CA9161,color:#fff
    style Impl fill:#CA9161,color:#fff

Code:

defmodule MyModule do
  # Module documentation (special reserved attribute)
  @moduledoc """
  This module demonstrates module attributes.
  Module attributes are compile-time constants.
  """
  # => @moduledoc appears in generated documentation
  # => Accessible via Code.fetch_docs/1

  # Compile-time constants
  @default_timeout 5000
  # => Computed once during compilation
  # => Inlined wherever used (zero runtime cost)
  # => @default_timeout is 5000
  @version "1.0.0"
  # => String constant
  # => @version is "1.0.0"
  @max_retries 3
  # => Integer constant
  # => @max_retries is 3

  # Function documentation (special reserved attribute)
  @doc """
  Waits for a specified timeout or default.
  Returns :ok after waiting.
  """
  # => @doc appears in function documentation (h/1 in IEx)
  def wait(timeout \\ @default_timeout) do
    # => Default argument uses module attribute
    # => @default_timeout inlined to 5000 at compile time
    :timer.sleep(timeout)
    # => Sleeps for timeout milliseconds
    :ok
    # => Returns :ok atom
  end

  @doc """
  Gets the module version.
  """
  def version, do: @version
  # => Returns compile-time constant (inlined to "1.0.0")

  # Computed at compile time (not runtime!)
  @languages ["Elixir", "Erlang", "LFE"]
  # => List created once during compilation
  # => @languages is ["Elixir", "Erlang", "LFE"]
  @language_count length(@languages)
  # => length/1 executed at compile time!
  # => Result: 3 (computed once, inlined everywhere)
  # => @language_count is 3

  def supported_languages, do: @languages
  # => Returns ["Elixir", "Erlang", "LFE"] (compile-time constant)
  # => Inlines to ["Elixir", "Erlang", "LFE"] (no runtime lookup)
  def language_count, do: @language_count
  # => Returns 3 (compile-time computed)
  # => Inlines to 3 (no runtime computation)

  # Module registration (declares implemented behaviour)
  @behaviour :gen_server
  # => Compiler checks we implement all required callbacks
  # => Common in OTP applications

  # Accumulating values (each @ reassignment creates new value)
  @colors [:red, :blue]
  # => Initial value: [:red, :blue]
  @colors [:green | @colors]
  # => Reads previous @colors, prepends :green → [:green, :red, :blue]
  @colors [:yellow | @colors]
  # => Reads previous @colors, prepends :yellow → [:yellow, :green, :red, :blue]

  def colors, do: @colors
  # => [:yellow, :green, :red, :blue] (final accumulated value)

  # Attributes are scoped to next function definition
  @important true
  # => Attribute active for next function
  # => @important is true (at this point)
  def func1, do: @important
  # => Returns true (attribute value when func1 defined)
  # => Captures @important value true

  @important false
  # => Redefines @important for next function
  # => @important is now false (at this point)
  def func2, do: @important
  # => Returns false (NEW value, doesn't affect func1!)
  # => Captures @important value false

  # Custom attributes for metadata
  @deprecated_message "Use new_function/1 instead"
  # => Custom attribute (not reserved, any compile-time value)

  @doc @deprecated_message
  # => Uses custom attribute in @doc (dynamic documentation)
  def old_function, do: :deprecated

  # Reserved attributes (special compiler meaning):
  # @moduledoc - module documentation
  # @doc - function documentation
  # @behaviour - declares implemented behaviour
  # @impl - marks callback implementation
  # @deprecated - marks deprecated (compiler warns)
  # @spec - type specification (dialyzer)
  # @type - defines custom type
  # @opaque - defines opaque type
  # => Custom attributes allowed: @author, @since, etc.
end

# Module attribute usage examples
MyModule.wait(1000)
# => Sleeps 1000ms, returns :ok

MyModule.version()
# => "1.0.0" (compile-time constant inlined)

MyModule.supported_languages()
# => ["Elixir", "Erlang", "LFE"] (compile-time constant)

MyModule.language_count()
# => 3 (computed once at compile time, not runtime!)

MyModule.colors()
# => [:yellow, :green, :red, :blue]
# => Final accumulated value from compile-time build

Key Takeaway: Module attributes (@name) are compile-time constants useful for documentation, configuration, and computed values. They’re evaluated during compilation, not runtime.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 38: Import, Alias, Require

import, alias, and require control how modules are referenced in your code. They reduce verbosity and manage namespaces cleanly.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Alias["alias MyApp.User"] --> Short["User -> MyApp.User<br/>Shorten module names"]
    Import["import Enum, only: [map: 2]"] --> NoPrefix["map(list, fn) -> Enum.map<br/>Remove module prefix"]
    Require["require Logger"] --> Macros["Logger.info -> Macro<br/>Enable macro expansion"]

    ModLevel["Module-level<br/>(outside def)"] --> AllFuncs["Available in all functions"]
    FuncLevel["Function-level<br/>(inside def)"] --> OnlyFunc["Available in that function only"]

    style Alias fill:#0173B2,color:#fff
    style Import fill:#DE8F05,color:#fff
    style Require fill:#029E73,color:#fff
    style Short fill:#0173B2,color:#fff
    style NoPrefix fill:#DE8F05,color:#fff
    style Macros fill:#029E73,color:#fff
    style ModLevel fill:#CC78BC,color:#fff
    style FuncLevel fill:#CA9161,color:#fff
    style AllFuncs fill:#CC78BC,color:#fff
    style OnlyFunc fill:#CA9161,color:#fff

Directive Comparison

DirectivePurposeWhen to Use
aliasShorten module namesAlways (safe, compile-time)
importBring functions into scopeSparingly (namespace pollution risk)
requireEnable macrosOnly for macros (Logger, custom macros)

Scoping Rules

  • Module-level: Affects all functions in module
  • Function-level: Only affects that function
  • import/require: Function-level when inside def, module-level when outside

Code:

defmodule ImportAliasRequire do               # => Module demonstrating namespace directives
  alias MyApp.Accounts.User                   # => Creates shorthand alias
  # => User = MyApp.Accounts.User (module-level scope)

  alias MyApp.Accounts.Admin, as: A           # => Custom alias with as: option
  # => A = MyApp.Accounts.Admin (different name)

  def create_user(name) do                    # => Function using User alias
    %User{name: name}                         # => Struct expansion via alias
    # => Expands to %MyApp.Accounts.User{name: name}
  end

  def create_admin(name) do                   # => Function using A alias
    %A{name: name}                            # => Struct expansion via custom alias
    # => Expands to %MyApp.Accounts.Admin{name: name}
  end

  import Enum, only: [map: 2, filter: 2]     # => Selective import (best practice)
  # => Brings map/2 and filter/2 into scope (no other Enum functions)

  def process_numbers(list) do                # => Function using imported functions
    list                                      # => Input list
    |> map(fn x -> x * 2 end)                # => Calls Enum.map/2 without module prefix
                                              # => Doubles each element
    |> filter(fn x -> x > 10 end)            # => Calls Enum.filter/2 without prefix
                                              # => Keeps only values > 10
  end

  import String, except: [split: 1]          # => Import all except split/1
  # => All String functions available except split/1

  def upcase_string(str) do                  # => Function using imported String function
    upcase(str)                               # => Calls String.upcase/1 without prefix
    # => Returns uppercased string
  end

  require Logger                              # => Required for macros
  # => Logger.info, Logger.debug are macros (compile-time expansion)

  def log_something do                        # => Function using Logger macro
    Logger.info("This is a log message")      # => Macro expands at compile time
    # => Generates log output with metadata
  end

  alias MyApp.{Accounts, Billing, Reports}   # => Multi-alias syntax
  # => Creates three aliases: Accounts, Billing, Reports

  def get_account_report do                   # => Function using multiple aliases
    account = Accounts.get()                  # => Expands to MyApp.Accounts.get/0
    # => Retrieves account data
    billing = Billing.get()                   # => Expands to MyApp.Billing.get/0
    # => Retrieves billing data
    Reports.generate(account, billing)        # => Expands to MyApp.Reports.generate/2
    # => Generates combined report
  end
end

defmodule MyApp.Accounts.User do              # => User struct definition
  defstruct name: nil, email: nil             # => Fields: name and email
end

defmodule MyApp.Accounts.Admin do             # => Admin struct definition
  defstruct name: nil, role: :admin           # => Fields: name and role (default :admin)
end

defmodule ScopingExample do                   # => Module demonstrating import scoping
  def func1 do                                # => Function with local import
    import Enum                               # => Function-level import (lexical scope)
    # => Import valid only within func1 body
    map([1, 2, 3], fn x -> x * 2 end)        # => Calls imported map/2
    # => Returns [2, 4, 6]
  end

  def func2 do                                # => Function without import
    Enum.map([1, 2, 3], fn x -> x * 2 end)   # => Must use full module name
    # => func1's import not available (function-scoped)
  end

  import String                               # => Module-level import
  # => Available in all functions below (module scope)

  def func3, do: upcase("hello")             # => Uses module-level imported upcase/1
  # => Returns "HELLO"

  def func4, do: downcase("WORLD")           # => Uses module-level imported downcase/1
  # => Returns "world"
end

# ImportAliasRequire.create_user("Alice")   # => Calls create_user/1
# => %MyApp.Accounts.User{name: "Alice", email: nil}

# ImportAliasRequire.process_numbers([1, 2, 3, 4, 5, 6, 7, 8])  # => Process with imported functions
# => Doubles: [2, 4, 6, 8, 10, 12, 14, 16]
# => Filters > 10: [12, 14, 16]

# ScopingExample.func1()                     # => Calls func1 with local import
# => [2, 4, 6]

# ScopingExample.func3()                     # => Calls func3 with module-level import
# => "HELLO"

Key Takeaway: Use alias to shorten module names, import to bring functions into scope (sparingly!), and require for macros. These directives manage namespaces and reduce verbosity.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 39: Protocols (Polymorphism)

Protocols enable polymorphism—defining a function that works differently for different data types. They’re Elixir’s mechanism for ad-hoc polymorphism, similar to interfaces in other languages.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Protocol["Protocol: Printable<br/>defines print/1"] --> ImplList["Implementation for List"]
    Protocol --> ImplMap["Implementation for Map"]
    Protocol --> ImplStruct["Implementation for User struct"]

    Call["Printable.print(data)"] --> Dispatch{Dispatch by type}
    Dispatch --> ImplList
    Dispatch --> ImplMap
    Dispatch --> ImplStruct

    style Protocol fill:#0173B2,color:#fff
    style ImplList fill:#DE8F05,color:#fff
    style ImplMap fill:#DE8F05,color:#fff
    style ImplStruct fill:#DE8F05,color:#fff
    style Call fill:#029E73,color:#fff
    style Dispatch fill:#CC78BC,color:#fff

Code:

defprotocol Printable do
  # => Protocol definition: defines polymorphic interface
  # => Protocols dispatch based on data type at runtime
  # => Similar to interfaces in OOP languages
  @doc "Converts data to a printable string"
  # => Documentation for protocol function
  def print(data)
  # => Function signature: takes any data, returns string
  # => Implementation provided by defimpl for each type
end

defimpl Printable, for: Integer do
  # => Implementation of Printable protocol for Integer type
  # => defimpl: define protocol implementation
  # => for: Integer: specifies target type
  def print(int), do: "Number: #{int}"
  # => Pattern matches integer parameter
  # => Returns formatted string with integer value
  # => String interpolation: #{int}
end

defimpl Printable, for: List do
  # => Implementation for List type
  def print(list), do: "List with #{length(list)} items: #{inspect(list)}"
  # => length(list): counts list elements
  # => inspect(list): converts list to readable string
  # => Returns formatted string with count and contents
end

defimpl Printable, for: Map do
  # => Implementation for Map type
  def print(map), do: "Map with #{map_size(map)} keys"
  # => map_size(map): counts keys in map (O(1) operation)
  # => Returns formatted string with key count
end

Printable.print(42)
# => Calls protocol function with integer
# => Runtime dispatch: finds Integer implementation
# => Executes Printable.Integer.print/1
# => Returns "Number: 42"

Printable.print([1, 2, 3])
# => Runtime dispatch: finds List implementation
# => Calls Printable.List.print/1
# => Returns "List with 3 items: [1, 2, 3]"

Printable.print(%{a: 1, b: 2})
# => Runtime dispatch: finds Map implementation
# => Calls Printable.Map.print/1
# => Returns "Map with 2 keys"

defmodule User do
  # => Custom struct definition
  defstruct name: nil, age: nil
  # => Struct with two fields: name and age
  # => Both default to nil
end

defimpl Printable, for: User do
  # => Protocol implementation for custom User struct
  # => Protocols work with any data type, including custom structs
  def print(user), do: "User: #{user.name}, age #{user.age}"
  # => Accesses struct fields: user.name, user.age
  # => Returns formatted string with user data
end

user = %User{name: "Alice", age: 30}
# => Creates User struct instance
# => Binds to user variable
# => %User{name: "Alice", age: 30}

Printable.print(user)
# => Runtime dispatch: finds User implementation
# => Calls Printable.User.print/1
# => Returns "User: Alice, age 30"

defimpl String.Chars, for: User do
  # => Implements built-in String.Chars protocol
  # => String.Chars: enables to_string/1 and string interpolation
  # => Implementing this protocol integrates User with Elixir's string system
  def to_string(user), do: user.name
  # => Returns user's name as string representation
  # => Used by to_string/1 and #{} interpolation
end

to_string(user)
# => Calls String.Chars.to_string/1
# => Dispatch finds User implementation
# => Returns "Alice"

"Hello, #{user}"
# => String interpolation calls to_string/1 implicitly
# => Uses String.Chars.User.to_string/1
# => Converts user to "Alice"
# => Returns "Hello, Alice"
# => Without String.Chars impl, would raise Protocol.UndefinedError

defmodule Range do
  # => Custom Range struct
  defstruct first: nil, last: nil
  # => Stores range bounds: first and last
end

defimpl Enumerable, for: Range do
  # => Implements built-in Enumerable protocol
  # => Enumerable: enables Enum.* functions (map, filter, reduce, count, etc.)
  # => Must implement: count/1, member?/2, reduce/3, slice/1

  def count(range), do: {:ok, range.last - range.first + 1}
  # => Returns total element count
  # => Formula: last - first + 1 (inclusive range)
  # => Example: first=1, last=5 => 5-1+1 = 5 elements
  # => Returns {:ok, count} tuple

  def member?(range, value), do: {:ok, value >= range.first and value <= range.last}
  # => Checks if value is in range
  # => Returns {:ok, boolean}
  # => Example: member?(1..5, 3) => {:ok, true}

  def reduce(range, acc, fun) do
    # => Core enumeration function
    # => Converts custom Range to built-in range (range.first..range.last)
    # => Delegates to Enum.reduce/3
    Enum.reduce(range.first..range.last, acc, fun)
    # => Applies fun to each element with accumulator
    # => This enables all Enum.* functions to work with Range
  end

  def slice(_range), do: {:error, __MODULE__}
  # => Slice operation not supported for this Range
  # => Returns {:error, module_name}
  # => __MODULE__: expands to current module name (Enumerable.Range)
  # => Some Enum functions (take, drop) use slice for optimization
  # => Error return means fallback to reduce-based implementation
end

my_range = %Range{first: 1, last: 5}
# => Creates Range struct with bounds 1..5
# => Represents values: [1, 2, 3, 4, 5]

Enum.count(my_range)
# => Calls Enumerable.Range.count/1
# => Returns {:ok, 5}
# => Enum.count extracts value from {:ok, count} tuple
# => Returns 5

Enum.member?(my_range, 3)
# => Calls Enumerable.Range.member?/2
# => Checks if 3 is in range 1..5
# => Returns {:ok, true}
# => Enum.member? extracts boolean from tuple
# => Returns true

Enum.map(my_range, fn x -> x * 2 end)
# => Uses Enumerable.Range.reduce/3 under the hood
# => Iterates: 1, 2, 3, 4, 5
# => Applies fn: 1*2=2, 2*2=4, 3*2=6, 4*2=8, 5*2=10
# => Returns [2, 4, 6, 8, 10]
# => This works because we implemented Enumerable protocol!

defprotocol Describable do
  # => Protocol with fallback to Any
  @fallback_to_any true
  # => @fallback_to_any: enables default implementation
  # => If no specific implementation found, uses Any implementation
  # => Without this, missing implementation raises Protocol.UndefinedError
  def describe(data)
  # => Function signature for polymorphic describe/1
end

defimpl Describable, for: Any do
  # => Fallback implementation for all types
  # => Only used if @fallback_to_any true
  # => Catches all types without specific implementation
  def describe(_data), do: "No description available"
  # => _data: unused parameter (underscore prefix)
  # => Returns generic fallback message
end

defimpl Describable, for: Integer do
  # => Specific implementation for Integer
  # => Takes precedence over Any implementation
  def describe(int), do: "The number #{int}"
  # => Returns specific description for integers
end

Describable.describe(42)
# => Runtime dispatch: finds Integer implementation
# => Uses specific Describable.Integer.describe/1 (not Any)
# => Returns "The number 42"

Describable.describe("hello")
# => Runtime dispatch: no String implementation found
# => Fallback to Any implementation (because @fallback_to_any true)
# => Calls Describable.Any.describe/1
# => Returns "No description available"

Describable.describe([1, 2, 3])
# => No List implementation found
# => Fallback to Any
# => Returns "No description available"
# => Without @fallback_to_any, would raise Protocol.UndefinedError

Key Takeaway: Protocols enable polymorphic functions that dispatch based on data type. Implement protocols for your custom types to integrate with Elixir’s built-in functions (to_string, Enum.*, etc.).

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 40: Result Tuples (:ok/:error)

Elixir idiomatically uses tagged tuples {:ok, value} or {:error, reason} to represent success and failure. This explicit error handling is preferred over exceptions for expected error cases.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Call["Function Call:<br/>divide(10, 2)"] --> Check{" Result?"}
    Check -->|Success| OK["{:ok, 5.0}<br/>Return success tuple"]
    Check -->|Failure| Error["{:error, :reason}<br/>Return error tuple"]

    Caller["Caller"] --> Match{"Pattern Match"}
    Match -->|" {:ok, value}"| HandleSuccess["Use value<br/>Continue execution"]
    Match -->|" {:error, reason}"| HandleError["Handle error<br/>Recover or propagate"]

    style Call fill:#0173B2,color:#fff
    style Check fill:#DE8F05,color:#fff
    style OK fill:#029E73,color:#fff
    style Error fill:#CC78BC,color:#fff
    style Caller fill:#0173B2,color:#fff
    style Match fill:#DE8F05,color:#fff
    style HandleSuccess fill:#029E73,color:#fff
    style HandleError fill:#CC78BC,color:#fff

Code:

defmodule ResultTuples do
  # => Module demonstrating idiomatic Elixir error handling
  # => Uses tagged tuples instead of exceptions for expected errors

  # Function that can succeed or fail
  def divide(a, b) when b != 0, do: {:ok, a / b}
  # => Clause 1: Guard checks b != 0
  # => Success case: returns {:ok, result} tuple
  # => Tagged tuple: :ok atom indicates success, second element is value
  # => Example: divide(10, 2) => {:ok, 5.0}

  def divide(_a, 0), do: {:error, :division_by_zero}
  # => Clause 2: Pattern matches b = 0 (exact value)
  # => Failure case: returns {:error, reason} tuple
  # => :division_by_zero is an atom describing the error
  # => _a: unused parameter (underscore prefix)

  # Parse integer from string
  def parse_int(string) do
    # => Wraps Integer.parse/1 with result tuple convention
    case Integer.parse(string) do
      # => Integer.parse/1 returns {int, rest} or :error
      {int, ""} -> {:ok, int}
      # => Pattern: {int, ""} means full string parsed (no remainder)
      # => Returns {:ok, int} indicating successful complete parse
      # => Example: parse("42") => {42, ""} => {:ok, 42}

      {_int, _rest} -> {:error, :partial_parse}
      # => Pattern: {int, rest} means partial parse (extra chars remain)
      # => Returns error indicating incomplete parse
      # => Example: parse("42abc") => {42, "abc"} => {:error, :partial_parse}

      :error -> {:error, :invalid_integer}
      # => Pattern: :error means string contains no valid integer
      # => Returns error indicating invalid input
      # => Example: parse("abc") => :error => {:error, :invalid_integer}
    end
  end

  # Fetch user from database (simulated)
  def fetch_user(id) when id > 0 and id < 100 do
    # => Clause 1: Guard validates id range [1, 99]
    # => Simulates successful database lookup
    {:ok, %{id: id, name: "User #{id}"}}
    # => Returns {:ok, map} with user data
  end

  def fetch_user(_id), do: {:error, :user_not_found}
  # => Clause 2: Catches all other ids (id <= 0 or id >= 100)
  # => Returns error tuple for invalid ids

  # Chain operations with pattern matching
  def get_user_name(id) do
    # => Demonstrates manual error propagation
    case fetch_user(id) do
      # => Call fetch_user, pattern match on result
      {:ok, user} -> {:ok, user.name}
      # => Success case: extract user.name, re-wrap in {:ok, ...}
      # => Propagates success with transformed value

      {:error, reason} -> {:error, reason}
      # => Failure case: propagate error unchanged
      # => Pattern matches any error reason, passes it through
    end
  end

  # Chain with `with`
  def calculate(a_str, b_str) do
    # => Demonstrates with expression for cleaner error chaining
    with {:ok, a} <- parse_int(a_str),
         # => Step 1: Parse first string to integer
         # => If {:ok, a} pattern matches: bind a, continue to step 2
         # => If {:error, _} or other: jump to else block

         {:ok, b} <- parse_int(b_str),
         # => Step 2: Parse second string to integer
         # => If {:ok, b} pattern matches: bind b, continue to step 3
         # => If error: jump to else block

         {:ok, result} <- divide(a, b) do
         # => Step 3: Divide a by b
         # => If {:ok, result} pattern matches: bind result, execute do block
         # => If error: jump to else block

      {:ok, result}
      # => All steps succeeded: return final result
      # => Returns {:ok, result} maintaining tuple convention
    else
      {:error, reason} -> {:error, reason}
      # => Any step failed: propagate error
      # => Pattern matches any error tuple from any step
      # => Early return: stops at first error
    end
  end
end

ResultTuples.divide(10, 2)
# => Calls divide/2 with valid divisor
# => Guard b != 0 succeeds (2 != 0 is true)
# => Returns {:ok, 5.0}

ResultTuples.parse_int("42")
# => Calls Integer.parse("42") => {42, ""}
# => Pattern matches {int, ""} (complete parse)
# => Returns {:ok, 42}

ResultTuples.fetch_user(1)
# => id = 1: guard id > 0 and id < 100 succeeds
# => Returns {:ok, %{id: 1, name: "User 1"}}

ResultTuples.divide(10, 0)
# => Calls divide/2 with zero divisor
# => Guard b != 0 fails (0 != 0 is false)
# => Falls through to clause 2: divide(_a, 0)
# => Returns {:error, :division_by_zero}

ResultTuples.parse_int("abc")
# => Calls Integer.parse("abc") => :error (no digits)
# => Pattern matches :error clause
# => Returns {:error, :invalid_integer}

ResultTuples.fetch_user(999)
# => id = 999: guard id > 0 and id < 100 fails (999 < 100 is false)
# => Falls through to clause 2: fetch_user(_id)
# => Returns {:error, :user_not_found}

ResultTuples.get_user_name(1)
# => Calls fetch_user(1) => {:ok, %{id: 1, name: "User 1"}}
# => Pattern matches {:ok, user}
# => Extracts user.name => "User 1"
# => Returns {:ok, "User 1"}

ResultTuples.get_user_name(999)
# => Calls fetch_user(999) => {:error, :user_not_found}
# => Pattern matches {:error, reason}
# => Returns {:error, :user_not_found} (propagated)

ResultTuples.calculate("10", "2")
# => with step 1: parse_int("10") => {:ok, 10} ✓ bind a=10
# => with step 2: parse_int("2") => {:ok, 2} ✓ bind b=2
# => with step 3: divide(10, 2) => {:ok, 5.0} ✓ bind result=5.0
# => All steps succeeded, execute do block
# => Returns {:ok, 5.0}

ResultTuples.calculate("10", "0")
# => with step 1: parse_int("10") => {:ok, 10} ✓ bind a=10
# => with step 2: parse_int("0") => {:ok, 0} ✓ bind b=0
# => with step 3: divide(10, 0) => {:error, :division_by_zero} ✗ mismatch!
# => Pattern {:ok, result} doesn't match {:error, ...}
# => Jump to else block
# => Returns {:error, :division_by_zero}

ResultTuples.calculate("abc", "2")
# => with step 1: parse_int("abc") => {:error, :invalid_integer} ✗ mismatch!
# => Pattern {:ok, a} doesn't match {:error, ...}
# => Jump to else block immediately (step 2 and 3 never execute)
# => Returns {:error, :invalid_integer}

case ResultTuples.divide(10, 2) do
  # => Pattern matching on result tuple
  # => Call divide(10, 2) => {:ok, 5.0}
  {:ok, result} -> IO.puts("Result: #{result}")
  # => Pattern matches {:ok, 5.0}, bind result=5.0
  # => Prints "Result: 5.0"
  # => Returns :ok (IO.puts return value)

  {:error, :division_by_zero} -> IO.puts("Cannot divide by zero")
  # => Would match if result was {:error, :division_by_zero}
  # => Not executed in this example
end

{:ok, value} = ResultTuples.divide(10, 2)
# => Pattern matching assignment
# => Right side: ResultTuples.divide(10, 2) => {:ok, 5.0}
# => Left side: {:ok, value} pattern
# => Pattern matches, binds value = 5.0
# => If pattern didn't match (e.g., returned {:error, ...}), raises MatchError

value
# => Returns 5.0 (extracted from tuple)
# => This unwraps the result, losing error information!
# => Use only when you're certain of success

defmodule Bang do
  # => Module demonstrating bang (!) function convention
  # => Bang functions unwrap results or raise exceptions

  def divide!(a, b) do
    # => Function name ends with !: indicates may raise exception
    # => Convention: ! functions unwrap {:ok, value} or raise on {:error, reason}
    case ResultTuples.divide(a, b) do
      # => Calls safe divide/2 that returns tuple
      {:ok, result} -> result
      # => Success: unwrap tuple, return bare value
      # => Converts {:ok, 5.0} to 5.0

      {:error, reason} -> raise "Division failed: #{reason}"
      # => Failure: raise RuntimeError with reason
      # => Converts {:error, :division_by_zero} to exception
      # => Caller must handle with try/rescue or let process crash
    end
  end
end

Bang.divide!(10, 2)
# => Calls ResultTuples.divide(10, 2) => {:ok, 5.0}
# => Pattern matches {:ok, result}, binds result=5.0
# => Returns 5.0 (unwrapped value)
# => No exception raised

Key Takeaway: Use tagged tuples {:ok, value} and {:error, reason} for expected error cases. Functions ending with ! unwrap results or raise exceptions. Pattern match to handle both success and failure cases.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 41: Try/Rescue/After

try/rescue/after handles exceptions. Use rescue to catch exceptions, after for cleanup code that always runs (like finally in other languages). Prefer result tuples for expected errors.

Code:

defmodule TryRescue do
  # => Module demonstrating exception handling with try/rescue/after
  # => Use for exceptions from external libraries or cleanup scenarios

  # Basic try/rescue
  def safe_divide(a, b) do
    # => Wraps risky division operation in try/rescue
    try do
      # => try block: code that might raise exception
      a / b
      # => Division by zero raises ArithmeticError in Elixir
      # => If successful, returns result (5.0 for 10/2)
    rescue
      # => rescue block: catches and handles exceptions
      ArithmeticError -> {:error, :division_by_zero}
      # => Pattern matches specific exception type
      # => Converts exception to {:error, reason} tuple
      # => No error information, just atom tag
    end
    # => try/rescue returns value from matched block
  end

  # Multiple rescue clauses
  def parse_and_double(str) do
    # => Demonstrates multiple rescue patterns
    try do
      str
      |> String.to_integer()
      # => String.to_integer/1 raises ArgumentError for invalid strings
      # => Example: "abc" raises ArgumentError
      |> Kernel.*(2)
      # => Kernel.*/2: multiplication operator as function
      # => Doubles the integer
    rescue
      # => Multiple rescue clauses: matched top-to-bottom
      ArgumentError -> {:error, :invalid_integer}
      # => Clause 1: Simple pattern, catches ArgumentError
      # => Returns error tuple without exception details

      err in RuntimeError -> {:error, {:runtime_error, err.message}}
      # => Clause 2: Named pattern with "in"
      # => err: binds exception struct
      # => RuntimeError: exception type to match
      # => err.message: accesses exception message field
      # => Returns error tuple with message
    end
  end

  # try/after for cleanup
  def read_file(path) do
    # => Demonstrates after block for guaranteed cleanup
    {:ok, file} = File.open(path, [:read])
    # => Opens file, pattern matches {:ok, file}
    # => If file doesn't exist, raises MatchError (no try/rescue here!)
    # => file: file handle/PID

    try do
      # => try block: risky file reading
      IO.read(file, :all)
      # => Reads entire file contents
      # => Returns string with file data
      # => Could raise if file handle invalid
    after
      # => after block: ALWAYS executes (success or exception)
      # => Similar to finally in other languages
      File.close(file)
      # => Closes file handle to free resource
      # => Runs even if IO.read raises exception
      # => Ensures no file handle leak
      # => after block return value is IGNORED
    end
    # => Returns IO.read result (string) if successful
    # => If exception raised, propagates after cleanup
  end

  # try/rescue/after all together
  def complex_operation do
    # => Demonstrates combining rescue and after
    try do
      # => try block: intentionally dangerous operation
      result = 10 / 0
      # => Division by zero raises ArithmeticError
      # => This line never executes
      {:ok, result}
      # => Would return {:ok, Infinity} if division succeeded (it won't)
    rescue
      # => rescue block: handles exceptions
      ArithmeticError -> {:error, :arithmetic_error}
      # => Catches ArithmeticError specifically
      # => Matches division by zero case
      # => Returns error tuple

      _ -> {:error, :unknown_error}
      # => Catch-all pattern: matches any exception
      # => _ discards exception value (not bound to variable)
      # => Fallback for unexpected exceptions
    after
      # => after block: runs regardless of success/failure
      IO.puts("Cleanup happens here")
      # => Prints to stdout
      # => Executes BEFORE return (after rescue clause)
      # => Return value ignored (function returns rescue result)
    end
    # => Execution order: try → rescue → after → return rescue value
  end

  # Catch specific exception type
  def handle_specific_error do
    # => Demonstrates accessing exception struct
    try do
      raise ArgumentError, message: "Invalid argument"
      # => raise: throws exception
      # => ArgumentError: exception module
      # => message: "...": sets exception message field
    rescue
      e in ArgumentError -> "Caught: #{e.message}"
      # => e in ArgumentError: binds exception to e variable
      # => e: exception struct %ArgumentError{message: "Invalid argument"}
      # => e.message: accesses message field
      # => Returns string with message (no {:error, ...} tuple)
    end
  end

  # Re-raise exception
  def logged_operation do
    # => Demonstrates logging + re-raising
    try do
      raise "Something went wrong"
      # => raise "string": creates RuntimeError with message
      # => Shorthand for: raise RuntimeError, message: "..."
    rescue
      e ->
        # => e: catches any exception (no "in" type restriction)
        # => Binds exception struct to e variable
        Logger.error("Error occurred: #{inspect(e)}")
        # => Log error before re-raising
        # => inspect(e): converts exception struct to string
        # => Side effect: logs to logger backend

        reraise e, __STACKTRACE__
        # => reraise: re-throws exception with original stacktrace
        # => e: exception struct to re-throw
        # => __STACKTRACE__: special variable with current stacktrace
        # => Preserves original error location for debugging
        # => Function does NOT return (exception propagates)
    end
  end
end

TryRescue.safe_divide(10, 2)
# => try block: 10 / 2 = 5.0 (success, no exception)
# => rescue block: NOT executed
# => Returns 5.0 (not wrapped in tuple)

TryRescue.safe_divide(10, 0)
# => try block: 10 / 0 raises ArithmeticError
# => rescue block: catches ArithmeticError
# => Returns {:error, :division_by_zero}

TryRescue.parse_and_double("5")
# => try: String.to_integer("5") => 5 (success)
# => try: 5 * 2 = 10
# => rescue: NOT executed
# => Returns 10

TryRescue.parse_and_double("abc")
# => try: String.to_integer("abc") raises ArgumentError
# => rescue: catches ArgumentError (first clause matches)
# => Returns {:error, :invalid_integer}

TryRescue.complex_operation()
# => try: 10 / 0 raises ArithmeticError
# => rescue: catches ArithmeticError => {:error, :arithmetic_error}
# => after: IO.puts("Cleanup happens here") executes
# => Prints "Cleanup happens here" to stdout
# => Returns {:error, :arithmetic_error}

TryRescue.handle_specific_error()
# => try: raise ArgumentError => raises with message
# => rescue: catches ArgumentError, binds to e
# => e.message => "Invalid argument"
# => Returns "Caught: Invalid argument"



defmodule HTTPClient do
  # => Example HTTP client with exception handling
  def get(url) do
    # => Simulates HTTP request with error handling
    try do
      # => try block: risky HTTP call
      {:ok, "Response from #{url}"}
      # => In real code, this would be: HTTPoison.get!(url)
      # => Returns success tuple with response
    rescue
      # => rescue with multiple exception types
      HTTPError -> {:error, :http_error}
      # => Hypothetical HTTPError exception
      # => Catches HTTP-specific errors (404, 500, etc.)

      TimeoutError -> {:error, :timeout}
      # => Hypothetical TimeoutError exception
      # => Catches connection timeout errors
      # => Different error handling based on exception type
    end
  end
end

Key Takeaway: Use try/rescue/after to handle exceptions from external libraries or for cleanup. Prefer result tuples for expected errors. The after block always runs, making it ideal for resource cleanup.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 42: Raise and Custom Exceptions

Use raise to throw exceptions. Define custom exception modules for domain-specific errors. Exceptions should be for unexpected situations, not control flow.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    SafeFunc["Safe Function:<br/>fetch(key)"] --> Check{" Key exists?"}
    Check -->|Yes| OkTuple["{:ok, value}<br/>Explicit success"]
    Check -->|No| ErrorTuple["{:error, :not_found}<br/>Explicit error"]

    BangFunc["Bang Function:<br/>fetch!(key)"] --> Check2{" Key exists?"}
    Check2 -->|Yes| Value["value<br/>Return value directly"]
    Check2 -->|No| Raise["raise KeyError<br/>Exception thrown"]

    Caller["Caller handles:"] --> Pattern["Pattern match<br/>{:ok, v} or {:error, r}"]
    Caller2["Caller handles:"] --> TryRescue["try/rescue<br/>or let it crash"]

    style SafeFunc fill:#0173B2,color:#fff
    style BangFunc fill:#0173B2,color:#fff
    style OkTuple fill:#029E73,color:#fff
    style ErrorTuple fill:#DE8F05,color:#fff
    style Value fill:#029E73,color:#fff
    style Raise fill:#CC78BC,color:#fff
    style Pattern fill:#029E73,color:#fff
    style TryRescue fill:#CC78BC,color:#fff

Exception Conventions

Custom Exceptions: Define with defexception (creates struct with __exception__: true) Message Protocol: Implement message/1 for formatted error messages Bang Functions: ! suffix indicates function may raise (e.g., fetch!/1 vs fetch/1) Return Tuples: Safe functions return {:ok, value} or {:error, reason}

Safe vs Bang Functions

PatternReturn TypeError HandlingUse Case
fetch/1{:ok, value} | {:error, reason}Explicit error tuplesCaller handles errors
fetch!/1valueRaises exceptionErrors unexpected or unrecoverable

Code:

defmodule MyApp.ValidationError do
  # => Custom exception with default values
  # => defexception creates struct with __exception__: true field
  defexception message: "Validation failed", field: nil
  # => Fields: message (string), field (atom or nil)
  # => field stores which field failed validation

  @impl true
  # => Implements Exception protocol's message/1 callback
  def message(exception) do
    # => Exception protocol callback for formatted messages
    # => Called when exception converted to string (e.g., in error logs)
    "Validation failed for field: #{exception.field}"
    # => Interpolates field name into message
    # => Returns formatted string for display
  end
end

defmodule MyApp.NotFoundError do
  # => Custom exception with list syntax (all fields default to nil)
  defexception [:resource, :id]
  # => Fields: resource (type), id (identifier)
  # => Creates struct: %MyApp.NotFoundError{resource: nil, id: nil}

  @impl true
  # => Implements Exception protocol message/1 callback
  def message(exception) do
    # => Custom message format
    # => Builds 404-style error message from struct fields
    "#{exception.resource} with id #{exception.id} not found"
    # => Human-readable 404-style error
    # => Returns: "User with id 999 not found"
  end
end

defmodule UserValidator do
  # => Validation module with bang functions (! means may raise)
  # => Bang convention: function may raise exception on invalid input

  def validate_age!(age) when is_integer(age) and age >= 0 and age < 150, do: :ok
  # => Happy path: age is integer in valid range [0, 150)
  # => Guard clause ensures: is_integer AND age >= 0 AND age < 150
  # => Returns: :ok (validation passed)

  def validate_age!(age) when is_integer(age) do
    # => Integer but out of range (age < 0 or age >= 150)
    # => Second clause matches when first guard fails
    raise MyApp.ValidationError, field: :age, message: "Age must be between 0 and 150, got: #{age}"
    # => Raises with specific error message
    # => Keyword list populates exception struct fields
  end

  def validate_age!(_age) do
    # => Catch-all: not an integer
    # => Third clause matches when type is wrong (string, float, etc.)
    raise MyApp.ValidationError, field: :age, message: "Age must be an integer"
    # => Raises type error with descriptive message
  end

  def validate_email!(email) when is_binary(email) do
    # => Email validation (is_binary checks string type)
    # => Guard ensures email is binary (string in Elixir)
    if String.contains?(email, "@") do
      :ok
      # => Success: email has @ symbol
      # => Returns :ok atom (validation passed)
    else
      raise MyApp.ValidationError, field: :email, message: "Email must contain @"
      # => Basic validation (production would use regex)
      # => Raises when @ missing from email string
    end
  end

  def validate_email!(_email) do
    # => Catch-all: email is not a string
    # => Matches when email is integer, atom, list, etc.
    raise MyApp.ValidationError, field: :email, message: "Email must be a string"
    # => Raises type error for non-string input
  end
end

UserValidator.validate_age!(30)
# => Valid age: returns :ok
# => 30 is integer in range [0, 150) - first clause matches

UserValidator.validate_email!("alice@example.com")
# => Valid email: returns :ok
# => String contains @ - validation passes

# Example: age validation failure (out of range)
# UserValidator.validate_age!(200)
# => Raises MyApp.ValidationError: "Age must be between 0 and 150, got: 200"
# => Second clause raises because 200 >= 150

# Example: age validation failure (wrong type)
# UserValidator.validate_age!("30")
# => Raises MyApp.ValidationError: "Age must be an integer"
# => Third clause raises because "30" is string, not integer

defmodule UserRepo do
  # => Repository demonstrating safe/bang function pairs
  # => fetch/1 returns tuple, fetch!/1 raises exception

  def fetch(id) when id > 0 and id < 100 do
    # => Safe fetch: returns result tuple
    # => Guard ensures ID in valid range (1-99)
    {:ok, %{id: id, name: "User #{id}"}}
    # => Success: {:ok, user_map}
    # => Returns tagged tuple with user data
  end

  def fetch(_id), do: {:error, :not_found}
  # => Catch-all: invalid ID returns error tuple
  # => Matches when id <= 0 or id >= 100
  # => Returns: {:error, :not_found} for explicit error handling

  def fetch!(id) do
    # => Bang version: unwraps result or raises
    # => Calls safe fetch/1, then pattern matches on result
    case fetch(id) do
      {:ok, user} -> user
      # => Success: unwrap tuple, return bare user map
      # => Extracts user from {:ok, user} tuple

      {:error, :not_found} ->
        # => Failure: raise custom exception
        # => Pattern matches error tuple from fetch/1
        raise MyApp.NotFoundError, resource: "User", id: id
        # => Exception message: "User with id <id> not found"
        # => Keyword list populates exception struct
    end
  end
end

UserRepo.fetch(1)
# => Returns: {:ok, %{id: 1, name: "User 1"}}
# => ID in range [1, 99] - first clause returns success tuple

UserRepo.fetch(999)
# => Returns: {:error, :not_found}
# => ID out of range - second clause returns error tuple

UserRepo.fetch!(1)
# => Returns: %{id: 1, name: "User 1"} (unwrapped)
# => fetch!/1 calls fetch/1, unwraps {:ok, user} tuple

# Example: bang function raising exception
# UserRepo.fetch!(999)
# => Raises MyApp.NotFoundError: "User with id 999 not found"
# => fetch!/1 calls fetch/1, matches {:error, :not_found}, raises exception

Key Takeaway: Raise exceptions for unexpected, unrecoverable errors. Define custom exceptions for domain-specific errors. Use the ! convention: functions ending with ! raise exceptions, non-bang versions return result tuples.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 43: Spawning Processes

Processes are Elixir’s lightweight concurrency primitive. Each process has its own memory and communicates via message passing. Use spawn/1 or spawn_link/1 to create processes.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Main["Main Process"] --> Spawn["spawn(fn -> ... end)"]
    Spawn --> P1["Process 1<br/>Isolated Memory"]
    Spawn --> P2["Process 2<br/>Isolated Memory"]
    Spawn --> P3["Process 3<br/>Isolated Memory"]

    Main --> Send["send(pid, message)"]
    Send --> P1
    Send --> P2
    Send --> P3

    style Main fill:#0173B2,color:#fff
    style Spawn fill:#DE8F05,color:#fff
    style P1 fill:#029E73,color:#fff
    style P2 fill:#029E73,color:#fff
    style P3 fill:#029E73,color:#fff
    style Send fill:#CC78BC,color:#fff

Process Lifecycle

Spawning: spawn/1 or spawn/3 creates new BEAM process with isolated memory Execution: Process runs concurrently (non-blocking), executes function, then exits Isolation: Each process has separate memory (data copied, not shared) Lifecycle: Check status with Process.alive?/1, inspect with Process.info/1

spawn/1 vs spawn_link/1

FunctionBehaviorUse Case
spawn/1Isolated processes (crashes don’t propagate)Independent tasks, fire-and-forget
spawn_link/1Linked processes (bidirectional crash propagation)Supervised tasks, dependent workers

Code:

# Basic process spawning
pid = spawn(fn -> IO.puts("Hello from spawned process!") end)
# => Creates new BEAM process executing function concurrently
# => Returns: PID (process identifier) e.g. #PID<0.150.0>
# => Process exits when function completes (non-blocking)

Process.alive?(pid)
# => Checks if process is still running
# => Returns: false (process finished in microseconds)

# Long-running process example
long_process = spawn(fn ->
  # => Process sleeps before completing
  :timer.sleep(1000)
  # => Blocks process for 1 second (parent continues immediately)
  IO.puts("Finished after 1 second")
end)
# => Returns: PID of long-running process

Process.alive?(long_process)
# => Returns: true (process still sleeping)

:timer.sleep(1500)
# => Parent sleeps 1.5s (ensures child finishes)

Process.alive?(long_process)
# => Returns: false (child exited ~0.5s ago)

# Getting current process PID
self()
# => Returns PID of current (calling) process
# => Example: #PID<0.100.0>

# Spawning multiple processes
pids = Enum.map(1..5, fn i ->
  # => Creates 5 concurrent processes
  spawn(fn -> IO.puts("Process #{i}") end)
  # => Processes run concurrently (order not guaranteed)
  # => i: captured from parent scope (closure)
end)
# => Returns: list of 5 PIDs [#PID<0.151.0>, ...]

# Using module function with spawn
defmodule Worker do
  def work(n) do
    # => Worker function simulating task processing
    IO.puts("Working on task #{n}")
    :timer.sleep(100)
    # => Simulates 100ms work
    IO.puts("Task #{n} done!")
  end
end

spawn(Worker, :work, [1])
# => spawn/3: spawns process calling Worker.work(1)
# => Equivalent to: spawn(fn -> Worker.work(1) end)
# => Returns: PID

# Linked processes (crash propagation)
parent_pid = self()
# => Stores parent PID for reference
child = spawn_link(fn ->
  # => spawn_link/1: creates bidirectional link
  # => If either crashes, both crash (unless trapping exits)
  :timer.sleep(500)
  raise "Child process crashed!"
  # => Child crashes, propagates to parent via link
  # => WARNING: Parent crashes unless trapping exits
end)
# => Returns: PID of linked child

# Process introspection
pid = spawn(fn -> :timer.sleep(5000) end)
# => Long-lived process (5s sleep) for inspection

Process.info(pid)
# => Returns: keyword list with process information
# => [{:registered_name, []}, {:current_function, {:timer, :sleep, 1}}, ...]
# => Returns nil if process not alive

Process.info(pid, :status)
# => Queries specific field (e.g., :status)
# => Returns: {:status, :waiting} (process in sleep)
# => States: :running, :runnable, :suspended, :garbage_collecting

# Mass process creation (demonstrates lightweight processes)
Enum.each(1..10_000, fn _ ->
  # => Creates 10,000 concurrent processes
  spawn(fn -> :timer.sleep(10_000) end)
  # => Each process sleeps 10s
  # => Memory per process: ~2KB (very lightweight)
  # => Total: ~20MB for 10,000 processes
end)
# => All processes run concurrently (BEAM scheduler distributes across cores)

# Memory isolation demonstration
defmodule Isolation do
  def demonstrate do
    # => Shows data is copied, not shared between processes
    list = [1, 2, 3]
    # => Parent has list [1, 2, 3]
    spawn(fn ->
      # => Child receives COPY of list (no shared memory)
      modified_list = [0 | list]
      # => Child prepends 0: [0, 1, 2, 3]
      # => Parent's list unchanged
      IO.inspect(modified_list, label: "Child process")
      # => Prints: Child process: [0, 1, 2, 3]
    end)
    :timer.sleep(100)
    # => Wait for child to print
    IO.inspect(list, label: "Parent process")
    # => Prints: Parent process: [1, 2, 3]
    # => Proves: processes have isolated memory
  end
end

Isolation.demonstrate()
# => Output shows parent and child have different lists
# => Memory isolation: no shared state between processes

Key Takeaway: Processes are lightweight, isolated, and communicate via messages. Use spawn/1 for independent processes, spawn_link/1 for linked processes. Elixir can run millions of processes concurrently.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 44: Send and Receive

Processes communicate by sending and receiving messages. Messages go into a process mailbox and are processed with receive. This is asynchronous message passing—the sender doesn’t block.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Sender["Sender Process"] --> Send["send(pid, {:msg, data})"]
    Send --> Mailbox["Receiver Mailbox<br/>[{:msg, data}]"]
    Mailbox --> Receive["receive do<br/>{:msg, data} -> ..."]
    Receive --> Process["Process Message"]

    style Sender fill:#0173B2,color:#fff
    style Send fill:#DE8F05,color:#fff
    style Mailbox fill:#029E73,color:#fff
    style Receive fill:#CC78BC,color:#fff
    style Process fill:#CA9161,color:#fff

Code:

# Basic bidirectional message passing
receiver = spawn(fn ->
  # => Spawns receiver process
  # => Process waits for incoming messages
  receive do
    # => receive block: pattern matches messages from mailbox
    # => Blocks until matching message arrives
    # => Messages queued in FIFO order
    {:hello, sender} -> send(sender, {:hi, self()})
    # => Pattern 1: matches {:hello, <sender_pid>}
    # => sender: extracted from tuple (caller's PID)
    # => send/2: sends message to sender
    # => self(): receiver's own PID
    # => Reply: {:hi, <receiver_pid>}
    # => Non-blocking send (returns immediately)

    {:goodbye, sender} -> send(sender, {:bye, self()})
    # => Pattern 2: matches {:goodbye, <sender_pid>}
    # => Different message type, different response
    # => Reply: {:bye, <receiver_pid>}
  end
  # => After processing one message, process exits
  # => No loop: single-message receiver
end)

send(receiver, {:hello, self()})
# => send/2: sends message to receiver process
# => Message: {:hello, <parent_pid>}
# => Goes into receiver's mailbox
# => Async: sender doesn't wait for processing
# => Returns: {:hello, <parent_pid>} (the message sent)

receive do
  # => Parent waits for reply from receiver
  {:hi, pid} -> IO.puts("Received hi from #{inspect(pid)}")
  # => Matches {:hi, <receiver_pid>}
  # => pid: bound to receiver's PID
  # => Prints: "Received hi from #PID<0.X.Y>"
  # => Process continues after receiving reply
end

# Receive with timeout (prevents infinite blocking)
spawn(fn ->
  # => Spawns process that waits for message with timeout
  receive do
    :message -> IO.puts("Got message")
    # => Matches :message atom
    # => Would print if message received within timeout
  after
    # => after clause: timeout handler
    # => Executes if no matching message arrives in time
    1000 -> IO.puts("No message received after 1 second")
    # => 1000ms timeout (1 second)
    # => Prints timeout message
    # => Process exits after timeout
  end
end)
# => Process waits 1 second, times out, prints, exits
# => No message sent, so timeout always triggers

# Multiple message patterns
receiver = spawn(fn ->
  # => New receiver with multiple pattern handlers
  receive do
    {:add, a, b} -> IO.puts("Sum: #{a + b}")
    # => Pattern: {:add, number, number}
    # => Extracts a and b from tuple
    # => Prints sum result

    {:multiply, a, b} -> IO.puts("Product: #{a * b}")
    # => Pattern: {:multiply, number, number}
    # => Different operation, same structure
    # => Prints product result

    unknown -> IO.puts("Unknown message: #{inspect(unknown)}")
    # => Catch-all pattern: matches any message
    # => unknown: binds to entire message
    # => inspect/1: converts term to readable string
    # => Handles unexpected message formats
  end
end)

send(receiver, {:add, 5, 3})
# => Sends {:add, 5, 3} to receiver
# => Matches first pattern
# => Prints: Sum: 8
# => Process exits after handling message

# Echo server (recursive receive loop)
defmodule Echo do
  # => Module implementing echo server pattern
  def loop do
    # => Recursive function: processes messages indefinitely
    receive do
      {:echo, msg, sender} ->
        # => Pattern: {:echo, message, sender_pid}
        # => Receives message and sender PID
        send(sender, {:reply, msg})
        # => Echoes message back to sender
        # => Reply: {:reply, <original_msg>}
        loop()
        # => Recursive call: continue receiving
        # => Tail-recursive: no stack growth
        # => Process remains alive for next message

      :stop -> :ok
      # => Stop signal: breaks recursion
      # => Returns :ok (function exits)
      # => Process terminates after receiving :stop
    end
  end
end

echo_pid = spawn(&Echo.loop/0)
# => spawn(&Fun/Arity): spawns with function reference
# => &Echo.loop/0: captures Echo.loop function
# => Echo server starts, waiting for messages
# => Returns: PID of echo server

send(echo_pid, {:echo, "Hello", self()})
# => Sends echo request with message "Hello"
# => Includes parent PID for reply
# => Message: {:echo, "Hello", <parent_pid>}

receive do
  {:reply, msg} -> IO.puts("Echo replied: #{msg}")
  # => Waits for echo reply
  # => Matches {:reply, "Hello"}
  # => Prints: "Echo replied: Hello"
end

send(echo_pid, {:echo, "World", self()})
# => Second echo request
# => Echo server still running (due to loop())
# => Message: {:echo, "World", <parent_pid>}

receive do
  {:reply, msg} -> IO.puts("Echo replied: #{msg}")
  # => Waits for second reply
  # => Matches {:reply, "World"}
  # => Prints: "Echo replied: World"
end

send(echo_pid, :stop)
# => Sends stop signal to echo server
# => Matches :stop pattern
# => Echo server exits (no more loop())
# => Process terminates cleanly

# FIFO message ordering
pid = self()
# => Get current process PID
send(pid, :first)
# => Send message 1 to self
# => Mailbox: [:first]
send(pid, :second)
# => Send message 2 to self
# => Mailbox: [:first, :second] (FIFO order)
send(pid, :third)
# => Send message 3 to self
# => Mailbox: [:first, :second, :third]

receive do: (:first -> IO.puts("1"))
# => Compact syntax: single-clause receive
# => Matches and removes :first from mailbox
# => Prints: 1
# => Mailbox after: [:second, :third]

receive do: (:second -> IO.puts("2"))
# => Matches and removes :second (now at head)
# => Prints: 2
# => Mailbox after: [:third]

receive do: (:third -> IO.puts("3"))
# => Matches and removes :third
# => Prints: 3
# => Mailbox after: [] (empty)
# => Demonstrates FIFO: messages processed in send order

# flush() - clear mailbox
send(self(), :msg1)
# => Send message to self
# => Mailbox: [:msg1]
send(self(), :msg2)
# => Mailbox: [:msg1, :msg2]

flush()
# => flush/0: prints all messages in mailbox
# => Removes all messages from mailbox
# => Prints: :msg1, :msg2
# => Mailbox after: [] (empty)
# => Useful for debugging and cleanup

# Asynchronous message sending (non-blocking)
pid = spawn(fn ->
  :timer.sleep(2000)
  # => Process sleeps for 2 seconds before receiving
  # => Simulates slow message processing
  receive do
    msg -> IO.puts("Received: #{inspect(msg)}")
    # => Pattern matches any message
    # => Will print after 2-second sleep
  end
end)

send(pid, :hello)
# => Sends message while process is sleeping
# => Message queued in mailbox immediately
# => send/2 returns instantly (doesn't wait for receive)
# => Returns: :hello (the message)

IO.puts("Sent message, continuing...")
# => Prints immediately after send (proves non-blocking)
# => Parent continues while child sleeps
# => Message will be processed in 2 seconds

# Messages can be any Elixir term
send(self(), {:tuple, 1, 2})
# => Tuple message
# => Mailbox: [{:tuple, 1, 2}]
send(self(), [1, 2, 3])
# => List message
# => Mailbox: [{:tuple, 1, 2}, [1, 2, 3]]
send(self(), %{key: "value"})
# => Map message
# => Mailbox: [..., %{key: "value"}]
send(self(), "string")
# => String message
# => Mailbox: [..., "string"]
send(self(), 42)
# => Integer message
# => Mailbox: [..., 42]

flush()
# => Prints all 5 messages
# => {:tuple, 1, 2}
# => [1, 2, 3]
# => %{key: "value"}
# => "string"
# => 42
# => Demonstrates: messages can be any Elixir term
# => Mailbox cleared after flush

Key Takeaway: Processes communicate via asynchronous message passing. send/2 puts messages in the mailbox, receive pattern matches and processes them. Messages are queued in FIFO order.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 45: Process Monitoring

Process monitoring allows you to detect when other processes crash or exit. Use Process.monitor/1 to watch a process and receive a message when it exits.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    subgraph Linking["Process Linking (Bidirectional)"]
        P1["Process A"] -->|link| P2["Process B"]
        P2 -->|link| P1
        P2Crash["Process B crashes 💥"] --> BothDie["Both processes terminate"]
    end

    subgraph Monitoring["Process Monitoring (Unidirectional)"]
        M1["Monitor Process"] -->|monitor| M2["Monitored Process"]
        M2Crash["Monitored crashes 💥"] --> DownMsg["Monitor receives<br/>{:DOWN, ref, :process, pid, reason}"]
        DownMsg --> MonitorSurvives["Monitor survives,<br/>handles :DOWN message"]
    end

    style P1 fill:#0173B2,color:#fff
    style P2 fill:#029E73,color:#fff
    style P2Crash fill:#CC78BC,color:#fff
    style BothDie fill:#CC78BC,color:#fff
    style M1 fill:#0173B2,color:#fff
    style M2 fill:#029E73,color:#fff
    style M2Crash fill:#CC78BC,color:#fff
    style DownMsg fill:#DE8F05,color:#fff
    style MonitorSurvives fill:#029E73,color:#fff

Monitoring vs Linking

FeatureMonitoringLinking
DirectionUnidirectional (one watches another)Bidirectional (both crash together)
Crash PropagationNo (monitor survives, gets :DOWN message)Yes (both processes crash)
Use CaseDetect failures, health checksSupervised tasks, dependent workers
Message:DOWN tuple with exit reason:EXIT signal (unless trapping)

Monitor Message Format

:DOWN message: {:DOWN, ref, :process, pid, reason}

  • ref: Monitor reference (from Process.monitor/1)
  • pid: Monitored process PID
  • reason: Exit reason (:normal, exception tuple, :killed, etc.)

Code:

# Basic process monitoring (crash detection)
pid = spawn(fn ->
  # => Spawns process that crashes after 1s
  # => Spawned process runs in separate memory space
  :timer.sleep(1000)
  # => Sleeps 1000ms (1 second) before crashing
  raise "Process crashed!"
  # => Raises RuntimeError, exits abnormally
  # => Exit reason: {:error, %RuntimeError{message: "Process crashed!"}}
end)
# => pid: spawned process identifier (e.g., #PID<0.123.0>)

ref = Process.monitor(pid)
# => Starts monitoring (unidirectional: this watches pid)
# => Returns: reference e.g. #Reference<0.1234.5678>
# => Monitor doesn't crash monitoring process (unlike link)
# => Reference uniquely identifies this monitor relationship

receive do
  {:DOWN, ^ref, :process, ^pid, reason} ->
    # => Pattern: {:DOWN, ref, :process, pid, exit_reason}
    # => ^ref, ^pid: pin operators ensure correct monitor/process
    # => Pin operators (^) match against captured values (not rebind)
    # => reason: crash reason (exception tuple + stacktrace)
    # => reason structure: {:error, %RuntimeError{...}}
    IO.puts("Process #{inspect(pid)} exited with reason: #{inspect(reason)}")
    # => Logs exit reason for debugging
after
  2000 -> IO.puts("No exit message received")
  # => Timeout: 2s (safety net)
  # => Matches if no :DOWN message within 2000ms
end
# => Monitor auto-removed after :DOWN received
# => Cleanup: no manual demonitor needed

# Monitoring normal exit
pid = spawn(fn ->
  # => Process exits normally after 0.5s
  # => Normal exit: function completes without raising
  :timer.sleep(500)
  # => Sleeps 500ms (0.5 seconds)
  :ok  # Normal exit
  # => Exits with reason :normal
  # => Last expression value becomes exit reason
end)
# => pid: normal-exit process identifier

ref = Process.monitor(pid)
# => Monitors normal and abnormal exits
# => :DOWN sent for both normal and crash exits

receive do
  {:DOWN, ^ref, :process, ^pid, reason} ->
    # => reason: :normal (not error tuple)
    # => Normal exit reason is atom :normal, not exception tuple
    IO.puts("Process exited normally with reason: #{inspect(reason)}")
    # => Prints: "Process exited normally with reason: :normal"
    # => inspect/1 converts atom to string representation
after
  1000 -> IO.puts("No exit")
  # => Timeout: 1s (should receive :DOWN within 500ms)
end
# => Monitor auto-removed after :DOWN received

# Demonitor - stop monitoring
pid = spawn(fn -> :timer.sleep(10_000) end)
# => Long-running process (10s)
# => Useful for testing manual demonitor
ref = Process.monitor(pid)
# => Start monitoring
# => ref: monitor reference to remove later
Process.demonitor(ref)  # Stop monitoring
# => Removes monitor, no :DOWN message sent
# => Process still runs, just not monitored
# => Manual cleanup before process exits
Process.exit(pid, :kill)  # Kill the process
# => :kill: brutal termination (cannot be trapped)
# => No :DOWN (already demonitored)
# => Process killed but monitoring process doesn't receive :DOWN

# Monitor multiple processes (parallel work tracker)
pids = Enum.map(1..5, fn i ->
  # => Creates 5 processes with staggered completion (100-500ms)
  spawn(fn ->
    :timer.sleep(i * 100)
    # => Process 1: 100ms, Process 2: 200ms, ..., Process 5: 500ms
    IO.puts("Process #{i} done")
  end)
end)
# => All processes run concurrently

refs = Enum.map(pids, &Process.monitor/1)
# => Monitor all processes
# => &Process.monitor/1: function capture syntax
# => refs: list of 5 monitor references

Enum.each(refs, fn ref ->
  # => Wait for each process to complete
  receive do
    {:DOWN, ^ref, :process, _pid, :normal} -> :ok
    # => Match specific ref (^ref pin operator)
    # => Expect normal exit
  end
end)
# => Total wait: ~500ms (concurrent, not sequential)
IO.puts("All processes finished")
# => Coordination pattern: wait for multiple tasks

# TimeoutHelper - production pattern for process timeout
defmodule TimeoutHelper do
  # => Implements timeout pattern with monitoring
  def call_with_timeout(fun, timeout) do
    # => Executes fun with timeout limit
    parent = self()
    # => Capture parent PID (child sends result here)
    pid = spawn(fn ->
      # => Spawn child to execute function
      result = fun.()
      # => Execute function (crashes child if fun raises)
      send(parent, {:result, self(), result})
      # => Send result to parent
    end)

    ref = Process.monitor(pid)
    # => Monitor child (detects crash before result sent)

    receive do
      {:result, ^pid, result} ->
        # => Success: function completed
        Process.demonitor(ref, [:flush])
        # => Cleanup: remove monitor and flush pending :DOWN
        {:ok, result}

      {:DOWN, ^ref, :process, ^pid, reason} ->
        # => Child crashed before completing
        {:error, {:process_died, reason}}
    after
      timeout ->
        # => Timeout: child too slow
        Process.exit(pid, :kill)
        # => Kill child (prevents zombie processes)
        Process.demonitor(ref, [:flush])
        # => Cleanup monitor
        {:error, :timeout}
    end
    # => Returns: {:ok, result} | {:error, {:process_died, reason}} | {:error, :timeout}
  end
end

# Success case - completes within timeout
TimeoutHelper.call_with_timeout(fn -> :timer.sleep(500); 42 end, 1000)  # => {:ok, 42}
# => 500ms < 1000ms: completes successfully

# Timeout case - exceeds timeout
TimeoutHelper.call_with_timeout(fn -> :timer.sleep(2000); 42 end, 1000)  # => {:error, :timeout}
# => 2000ms > 1000ms: exceeds timeout
# => Child killed, returns {:error, :timeout}

Key Takeaway: Use Process.monitor/1 to watch processes and receive :DOWN messages when they exit. Monitoring is unidirectional (unlike linking) and ideal for detecting process failures without crashing.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 46: Task Module (Async/Await)

The Task module provides a simple abstraction for spawning processes and awaiting results. It’s built on processes but handles boilerplate for async/await patterns.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Main["Main Process"] --> Async["Task.async(fn)"]
    Async --> Task1["Task Process<br/>Execute function"]
    Task1 --> Await["Task.await(task)"]
    Await --> Result["Return result to main"]

    Main2["Main Process"] --> AsyncStream["Task.async_stream"]
    AsyncStream --> Pool["Process Pool"]
    Pool --> T1["Task 1"]
    Pool --> T2["Task 2"]
    Pool --> T3["Task 3"]

    style Main fill:#0173B2,color:#fff
    style Main2 fill:#0173B2,color:#fff
    style Async fill:#DE8F05,color:#fff
    style Task1 fill:#029E73,color:#fff
    style Await fill:#CC78BC,color:#fff
    style AsyncStream fill:#DE8F05,color:#fff
    style Pool fill:#CA9161,color:#fff
    style T1 fill:#029E73,color:#fff
    style T2 fill:#029E73,color:#fff
    style T3 fill:#029E73,color:#fff

Task Functions Comparison

FunctionReturn TypeBlockingUse CaseCleanup
Task.async/1%Task{}NoAwait result laterLinked
Task.await/2resultYesGet task resultAuto
Task.yield/2{:ok, result} | nilNoPoll without errorManual
Task.start/1{:ok, pid}NoFire-and-forgetUnlinked
Task.async_stream/3Stream.t()NoParallel collectionsAuto

Timeout Behavior

  • Task.await/2: Raises Task.TimeoutError on timeout (default: 5000ms)
  • Task.yield/2: Returns nil on timeout (non-blocking check)
  • Orphaned tasks: Continue running after timeout (potential leak)

Code:

# Basic async/await pattern
task = Task.async(fn ->
  # => Spawns process to execute function concurrently
  # => Returns: %Task{pid: <pid>, ref: <ref>, owner: <owner>}
  :timer.sleep(1000)
  # => Simulates 1s computation (task runs, caller continues)
  42
  # => Return value for Task.await/1
end)
# => Task struct: %Task{pid: #PID<0.150.0>, ...}

IO.puts("Task started, doing other work...")
# => Prints immediately (non-blocking)

result = Task.await(task)  # => 42
# => Blocks until task completes (~1s wait)
# => Returns: task function's return value
IO.puts("Task result: #{result}")
# => Prints: "Task result: 42"

# Multiple parallel tasks
tasks = Enum.map(1..5, fn i ->
  # => Creates 5 concurrent tasks
  Task.async(fn ->
    # => Each task in separate process
    :timer.sleep(i * 200)
    # => Task 1: 200ms, Task 2: 400ms, ..., Task 5: 1000ms
    i * i
    # => Return square: 1, 4, 9, 16, 25
  end)
end)
# => 5 Task structs, all processes running concurrently

results = Enum.map(tasks, &Task.await/1)  # => [1, 4, 9, 16, 25]
# => Await all tasks sequentially
# => &Task.await/1: function capture syntax
# => Total wait: ~1000ms (concurrent, not 3000ms sequential)
IO.inspect(results)
# => Prints: [1, 4, 9, 16, 25]

# Task timeout with exception
task = Task.async(fn ->
  # => Spawns slow task (10 seconds)
  :timer.sleep(10_000)
  :done
  # => Never reached (timeout occurs first)
end)

try do
  Task.await(task, 1000)  # Timeout after 1 second
  # => Wait max 1000ms (task needs 10000ms)
  # => Raises Task.TimeoutError after 1s
  # => WARNING: Task process keeps running (orphaned)
rescue
  e in Task.TimeoutError ->
    # => Catches timeout exception
    IO.puts("Task timed out: #{inspect(e)}")
    # => Prints timeout error details
end
# => Task process still running in background

# Task.yield - non-blocking check
task = Task.async(fn ->
  # => Task takes 2 seconds
  :timer.sleep(2000)
  :result
end)

case Task.yield(task, 500) do
  # => Non-blocking: returns immediately after 500ms
  # => Does NOT raise exception on timeout
  {:ok, result} -> IO.puts("Got result: #{result}")
  # => Task completed within 500ms (won't match, needs 2000ms)
  nil -> IO.puts("Task still running after 500ms")
  # => Task not completed, returns nil (not error tuple)
  # => Prints: "Task still running after 500ms"
end
# => Task process still alive (continues running)

case Task.yield(task, 2000) do
  # => Second yield: wait up to 2000ms more
  # => Task has ~1500ms remaining (completes in time)
  {:ok, result} -> IO.puts("Got result: #{result}")
  # => Task completed, returns {:ok, :result}
  # => Prints: "Got result: result"
  nil -> IO.puts("Still running")
  # => Won't match (task completes in ~1500ms)
end
# => Total elapsed: ~2000ms (500ms + 1500ms)

# Task.start - fire and forget
Task.start(fn ->
  # => Spawns unlinked task (returns {:ok, pid}, not %Task{})
  # => No await needed (fire-and-forget pattern)
  :timer.sleep(1000)
  IO.puts("Background task completed")
  # => Prints after 1 second asynchronously
end)
# => Returns: {:ok, #PID<0.160.0>}
IO.puts("Main process continues immediately")
# => Prints immediately (doesn't wait)

# Task.async_stream - parallel collection processing
results = 1..10
          |> Task.async_stream(fn i ->
            # => Processes collection in parallel with bounded concurrency
            # => Preserves element ordering in results
            :timer.sleep(100)
            # => Each task sleeps 100ms
            i * i
            # => Compute square: 1, 4, 9, ..., 100
          end, max_concurrency: 4)
          # => At most 4 tasks run simultaneously
          # => As tasks complete, new tasks start (batched execution)
          # => Returns: stream of {:ok, result} tuples
          |> Enum.to_list()
          # => Force stream evaluation (blocks until all complete)
          # => Total time: ~300ms (10 elements / 4 concurrency * 100ms)
# => [{:ok, 1}, {:ok, 4}, {:ok, 9}, ..., {:ok, 100}]

# Error handling in tasks
task = Task.async(fn ->
  # => Task that crashes
  raise "Task error!"
  # => Task process crashes immediately
end)

try do
  Task.await(task)
  # => Task crashed, exception propagated to caller
  # => Re-raises RuntimeError in caller's context
rescue
  e -> IO.puts("Caught error: #{inspect(e)}")
  # => Catches %RuntimeError{message: "Task error!"}
end
# => Task process terminated (crashed)

# Task.Supervisor - supervised tasks
{:ok, result} = Task.Supervisor.start_link()
# => Starts task supervisor for managed task processes
# => Provides isolation: task crashes don't affect caller
# => Returns: {:ok, #PID<supervisor_pid>}

Key Takeaway: Task provides async/await abstraction over processes. Use Task.async/1 and Task.await/1 for parallel work with results. Use Task.async_stream/3 for processing collections in parallel.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 47: ExUnit Basics

ExUnit is Elixir’s built-in testing framework. Tests are organized into test modules, and assertions verify expected behavior. Running mix test executes all tests in your project.

Key ExUnit Features

FeaturePurposeExample
use ExUnit.CaseImport test macrosRequired in test modules
test "description"Define test casetest "adds numbers" do ... end
assertVerify truthy valueassert 1 + 1 == 2
refuteVerify falsy valuerefute 1 > 2
assert_raiseVerify exceptionassert_raise Error, fn -> ... end
setupPer-test initializationRuns before each test
setup_allOne-time initializationRuns before all tests
@tagCategorize tests@tag :slow for selective runs

Test Organization

  • Module naming: <ModuleName>Test convention
  • File location: test/ directory
  • Discovery: mix test auto-discovers test modules
  • Execution: Tests run in random order (ensures independence)
  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Setup["setup do<br/>..."] --> Test1["test 'description' do<br/>..."]
    Test1 --> Assert1["assert value == expected"]
    Assert1 --> Teardown["(automatic cleanup)"]

    Setup --> Test2["test 'another' do<br/>..."]
    Test2 --> Assert2["refute condition"]
    Assert2 --> Teardown

    style Setup fill:#0173B2,color:#fff
    style Test1 fill:#DE8F05,color:#fff
    style Test2 fill:#DE8F05,color:#fff
    style Assert1 fill:#029E73,color:#fff
    style Assert2 fill:#029E73,color:#fff
    style Teardown fill:#CC78BC,color:#fff

Code:

defmodule MathTest do                  # => Defines test module MathTest
  use ExUnit.Case                       # => Imports test macros (test, assert, refute, setup)
                                        # => Required in all test modules

  test "addition works" do              # => Defines test case with description
                                        # => Each test is independent
    assert 1 + 1 == 2                   # => Asserts 1 + 1 equals 2
                                        # => Passes if expression evaluates to true
                                        # => Returns true on success
  end                                   # => End test definition

  test "subtraction works" do           # => Second test case (independent)
                                        # => Tests run in random order for isolation
    assert 5 - 3 == 2                   # => Asserts 5 - 3 equals 2
                                        # => Returns true on success
  end                                   # => End test definition

  test "multiplication and division" do # => Test with multiple assertions
                                        # => All assertions must pass
    assert 2 * 3 == 6                   # => Asserts multiplication result
                                        # => 2 * 3 = 6, returns true
    assert 10 / 2 == 5.0                # => Asserts division result
                                        # => Division (/) always returns float
                                        # => 10 / 2 = 5.0 (not 5)
    assert rem(10, 3) == 1              # => Asserts remainder result
                                        # => rem/2 calculates modulo
                                        # => 10 % 3 = 1, returns true
  end                                   # => End test definition

  test "boolean assertions" do          # => Test demonstrating assert/refute
    assert true                         # => Asserts true is truthy
                                        # => Always passes
    refute false                        # => Refutes false (inverse of assert)
                                        # => Passes if expression is falsy
                                        # => Returns true on success
    assert 1 < 2                        # => Asserts comparison is true
                                        # => 1 < 2 evaluates to true
    refute 1 > 2                        # => Refutes comparison
                                        # => 1 > 2 is false, so refute passes
  end                                   # => End test definition

  test "pattern matching" do            # => Test using pattern matching in assertions
    assert {:ok, value} = {:ok, 42}     # => Pattern matches and binds value
                                        # => {:ok, value} matches {:ok, 42}
                                        # => value binds to 42
                                        # => Returns true on successful match
    assert value == 42                  # => Asserts value equals 42
                                        # => value was bound to 42 in previous line
                                        # => Returns true
  end                                   # => End test definition

  test "raises exception" do            # => Test verifying exception is raised
    assert_raise ArithmeticError, fn -> # => Expects ArithmeticError exception
                                        # => Takes exception type and function
      1 / 0                             # => Division by zero operation
                                        # => Raises ArithmeticError
                                        # => Test passes if error raised
    end                                 # => End assert_raise block
                                        # => Returns true if exception raised
  end                                   # => End test definition

  test "raises with message" do         # => Test verifying exception with message
    assert_raise ArgumentError, "Invalid", fn ->  # => Expects specific message
                                                   # => Verifies exception type AND message
      raise ArgumentError, "Invalid"    # => Raises ArgumentError with message "Invalid"
                                        # => Message must match exactly
                                        # => Test passes if both match
    end                                 # => End assert_raise block
                                        # => Returns true if exception and message match
  end                                   # => End test definition

  setup do                              # => Setup callback runs before EACH test
                                        # => Used for per-test initialization
    {:ok, user: %{name: "Alice", age: 30}}  # => Returns context map
                                             # => Tagged tuple with :ok
                                             # => user key contains map with name and age
                                             # => Available to all tests in this module
  end                                   # => End setup definition

  test "uses setup data", %{user: user} do  # => Test receives context from setup
                                             # => Pattern matches user from context map
                                             # => user binds to %{name: "Alice", age: 30}
    assert user.name == "Alice"         # => Accesses name field from user map
                                        # => user.name is "Alice"
                                        # => Returns true
    assert user.age == 30               # => Accesses age field from user map
                                        # => user.age is 30
                                        # => Returns true
  end                                   # => End test definition
end                                     # => End module definition

defmodule Calculator do                 # => Defines Calculator module for testing
  def add(a, b), do: a + b              # => Addition function (one-line syntax)
                                        # => Returns sum of a and b
  def subtract(a, b), do: a - b         # => Subtraction function
                                        # => Returns difference of a and b
  def multiply(a, b), do: a * b         # => Multiplication function
                                        # => Returns product of a and b
  def divide(_a, 0), do: {:error, :division_by_zero}  # => First clause: catches div by zero
                                                       # => Pattern matches when b is 0
                                                       # => Returns error tuple
  def divide(a, b), do: {:ok, a / b}    # => Second clause: normal division
                                        # => Only called if b is not 0
                                        # => Returns ok tuple with float result
end                                     # => End module definition

defmodule CalculatorTest do             # => Test module for Calculator
  use ExUnit.Case                       # => Imports test macros
                                        # => Required for test definitions

  test "add/2 adds two numbers" do      # => Test Calculator.add/2 function
                                        # => Verifies addition works correctly
    assert Calculator.add(2, 3) == 5    # => Calls add with 2 and 3
                                        # => 2 + 3 = 5, returns true
    assert Calculator.add(-1, 1) == 0   # => Tests negative numbers
                                        # => -1 + 1 = 0, returns true
  end                                   # => End test definition

  test "divide/2 returns ok tuple" do   # => Test successful division
                                        # => Verifies normal case returns :ok
    assert Calculator.divide(10, 2) == {:ok, 5.0}  # => Calls divide with 10 and 2
                                                    # => Second clause matches (b != 0)
                                                    # => 10 / 2 = 5.0 (float)
                                                    # => Returns {:ok, 5.0}
                                                    # => Assertion passes
  end                                   # => End test definition

  test "divide/2 handles division by zero" do  # => Test error handling
                                               # => Verifies div by zero returns error
    assert Calculator.divide(10, 0) == {:error, :division_by_zero}  # => Calls divide with b=0
                                                                      # => First clause matches (b is 0)
                                                                      # => Returns {:error, :division_by_zero}
                                                                      # => Assertion passes
  end                                   # => End test definition

  @tag :slow                            # => Module attribute tags test
                                        # => Categorizes as :slow test
                                        # => Use for selective test execution
  test "slow test" do                   # => Test with :slow tag
    :timer.sleep(2000)                  # => Sleeps for 2000ms (2 seconds)
                                        # => Simulates slow operation
                                        # => Run with: mix test --only slow
                                        # => Exclude with: mix test --exclude slow
    assert true                         # => Always passes
                                        # => Returns true
  end                                   # => End test definition

  @tag :skip                            # => Tags test to skip
                                        # => Test will not run by default
  test "skipped test" do                # => Skipped test (won't execute)
    assert false                        # => This would fail if executed
                                        # => Run with: mix test --include skip
                                        # => Useful for work-in-progress tests
  end                                   # => End test definition
end                                     # => End module definition

Key Takeaway: ExUnit provides testing with assert, refute, and assert_raise. Tests are organized in modules with use ExUnit.Case. Use setup for per-test initialization and tags to organize tests.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 48: Mix Project Structure

Mix is Elixir’s build tool. It manages dependencies, compiles code, runs tests, and provides project scaffolding. Understanding the standard project structure is essential for Elixir development.

Standard Project Structure

my_app/
├── mix.exs              # Project configuration
├── lib/                 # Application code
│   └── my_app.ex        # Main module
├── test/                # Test files
│   └── my_app_test.exs  # Tests
└── config/              # Configuration
    ├── config.exs       # Default config
    └── dev.exs          # Environment-specific

mix.exs Key Fields

FieldPurposeExample
appApplication name:my_app
versionSemantic version"0.1.0"
elixirMin Elixir version"~> 1.15"
depsDependencies list[{:jason, "~> 1.4"}]
extra_applicationsOTP apps to start[:logger]

Code:

# mix.exs - project configuration file at project root
                                        # => Required for all Mix projects
defmodule MyApp.MixProject do           # => Mix project module (convention: AppName.MixProject)
  use Mix.Project                       # => Imports Mix.Project behavior
                                        # => Provides project/0 and application/0 callbacks
                                        # => Required for Mix to recognize this as project

  def project do                        # => Returns keyword list of project config
                                        # => Called by Mix during compilation
    [                                   # => Keyword list starts here
      app: :my_app,                     # => Application name as atom
                                        # => Used for .beam file naming
                                        # => Must match folder name convention
      version: "0.1.0",                 # => Semantic version string
                                        # => Format: MAJOR.MINOR.PATCH
                                        # => Incremented when releasing
      elixir: "~> 1.15",                # => Minimum Elixir version requirement
                                        # => ~> is pessimistic constraint operator
                                        # => ~> 1.15 allows 1.15.x but not 1.16.0
                                        # => Ensures compatibility
      start_permanent: Mix.env() == :prod,  # => Determines application restart strategy
                                             # => Mix.env() returns :dev, :test, or :prod
                                             # => true in :prod: restart on crash
                                             # => false in :dev/:test: don't restart
      deps: deps()                      # => Calls deps/0 function below
                                        # => Returns list of dependencies
    ]                                   # => End keyword list
  end                                   # => End project/0 function

  def application do                    # => Returns OTP application configuration
                                        # => Called when starting application
    [                                   # => Keyword list for application config
      extra_applications: [:logger]     # => OTP applications to start BEFORE this app
                                        # => :logger starts Elixir's logging system
                                        # => Runs in application's supervision tree
    ]                                   # => End keyword list
  end                                   # => End application/0 function

  defp deps do                          # => Private function returns dependency list
                                        # => Called by project/0 deps: field
    [                                   # => List of dependency tuples
      {:httpoison, "~> 2.0"},           # => HTTP client library
                                        # => Tuple: {package_atom, version_requirement}
                                        # => ~> 2.0 allows 2.x (not 3.0)
      {:jason, "~> 1.4"},               # => JSON encoder/decoder library
                                        # => ~> 1.4 allows 1.4.x (not 1.5)
      {:ex_doc, "~> 0.30", only: :dev}  # => Documentation generator
                                        # => only: :dev means dev environment only
                                        # => Not included in :prod builds
                                        # => Reduces production dependencies
    ]                                   # => Fetched from Hex.pm package registry
                                        # => Downloaded to deps/ folder
  end                                   # => End deps/0 function
end                                     # => End module definition

# lib/my_app.ex - main application module
                                        # => Located in lib/ directory (convention)
defmodule MyApp do                      # => Main module for application
  @moduledoc """                        # => Module documentation attribute
  Documentation for `MyApp`.            # => Markdown syntax supported
  """                                   # => End module doc string
                                        # => Appears in generated ExDoc
                                        # => Accessible via IEx h(MyApp)

  @doc """                              # => Function documentation attribute
  Hello world function.                 # => Describes what function does
  """                                   # => End function doc string
                                        # => Appears in ExDoc and IEx h(MyApp.hello)
  def hello do                          # => Public function (no parameters)
                                        # => Callable as MyApp.hello()
    :world                              # => Returns atom :world
                                        # => Last expression is return value
                                        # => Type: atom
  end                                   # => End function definition
end                                     # => End module definition

# test/my_app_test.exs - test file
                                        # => Located in test/ directory
                                        # => .exs extension: not compiled (evaluated)
defmodule MyAppTest do                  # => Test module (convention: ModuleNameTest)
  use ExUnit.Case                       # => Imports ExUnit test macros
                                        # => Enables test, assert, refute
  doctest MyApp                         # => Runs doctests from @doc comments
                                        # => Extracts code examples from docs
                                        # => Verifies examples work as shown
                                        # => Keeps docs in sync with code

  test "greets the world" do            # => Test case definition
                                        # => Description: "greets the world"
    assert MyApp.hello() == :world      # => Calls MyApp.hello() function
                                        # => Expects return value :world
                                        # => Test passes if assertion true
  end                                   # => End test definition
end                                     # => End module definition

# config/config.exs - application configuration file
                                        # => Located in config/ directory
import Config                           # => Imports Config module
                                        # => Provides config/2 and config/3 macros
                                        # => Required for configuration

config :my_app,                         # => Configures :my_app application
                                        # => First arg: application atom
  api_key: "development_key",           # => Sets api_key config value
                                        # => String value for development
                                        # => Retrieved via Application.get_env/2
  timeout: 5000                         # => Sets timeout config value
                                        # => Integer 5000 (5 seconds)
                                        # => Accessible at runtime

import_config "#{Mix.env()}.exs"        # => Dynamically imports environment-specific config
                                        # => Mix.env() returns :dev, :test, or :prod
                                        # => Loads dev.exs, test.exs, or prod.exs
                                        # => Environment configs override this file

# Runtime config access examples
                                        # => These show how to READ config at runtime
api_key = Application.get_env(:my_app, :api_key)  # => Reads api_key from :my_app config
                                                    # => Returns "development_key"
                                                    # => Returns nil if not set
timeout = Application.get_env(:my_app, :timeout, 3000)  # => Reads timeout with default
                                                         # => Returns 5000 (configured value)
                                                         # => Returns 3000 if not configured
                                                         # => Third arg is default value

Key Takeaway: Mix provides project scaffolding, dependency management, and build tools. Standard structure: lib/ for code, test/ for tests, mix.exs for configuration. Use mix commands to compile, test, and manage projects.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 49: Doctests

Doctests embed tests in documentation using @doc comments. They verify that code examples in documentation actually work, keeping docs accurate and tested.

Code:

defmodule StringHelper do
  # => Module with string manipulation functions
  # => Demonstrates doctests in @doc comments
  @moduledoc """
  Helper functions for string manipulation.
  """
  # => @moduledoc: module-level documentation
  # => Brief description of module purpose
  # => Appears in generated docs and IEx (h StringHelper)

  @doc """
  Reverses a string.

  ## Examples

      iex> StringHelper.reverse("hello")
      "olleh"

      iex> StringHelper.reverse("Elixir")
      "rixilE"

      iex> StringHelper.reverse("")
      ""
  """
  # => @doc: function-level documentation
  # => ## Examples section: contains doctests
  # => iex>: IEx prompt (indicates doctest line)
  # => Next line: expected output
  # => doctest/1 in test file extracts and runs these
  # => 3 examples test: normal case, different input, edge case (empty string)
  def reverse(string) do
    # => reverse/1: reverses string character by character
    # => Parameter: string (binary)
    String.reverse(string)
    # => String.reverse/1: built-in function
    # => Returns: reversed string
    # => "hello" → "olleh"
  end
  # => Function implementation matches doctest expectations
  # => Doctests verify function works as documented

  @doc """
  Counts words in a string.

  ## Examples

      iex> StringHelper.word_count("hello world")
      2

      iex> StringHelper.word_count("one")
      1

      iex> StringHelper.word_count("")
      0
  """
  # => ## Examples: 3 doctests
  # => Test cases: multiple words, single word, empty string
  # => Expected outputs: 2, 1, 0
  def word_count(string) do
    # => word_count/1: counts words separated by whitespace
    string
    |> String.split()
    # => String.split/1: splits on whitespace (default)
    # => "hello world" → ["hello", "world"]
    # => "" → []
    |> length()
    # => length/1: counts list elements
    # => ["hello", "world"] → 2
    # => [] → 0
  end
  # => Returns: word count (integer)
  # => Doctests verify: 2 words, 1 word, 0 words

  @doc """
  Capitalizes each word.

  ## Examples

      iex> StringHelper.capitalize_words("hello world")
      "Hello World"

      iex> StringHelper.capitalize_words("ELIXIR programming")
      "Elixir Programming"
  """
  # => ## Examples: 2 doctests
  # => Test cases: lowercase, mixed case
  # => Both should capitalize first letter of each word
  def capitalize_words(string) do
    # => capitalize_words/1: capitalizes first letter of each word
    string
    |> String.split()
    # => Split into words: "hello world" → ["hello", "world"]
    # => "ELIXIR programming" → ["ELIXIR", "programming"]
    |> Enum.map(&String.capitalize/1)
    # => Capitalize each word: ["hello", "world"] → ["Hello", "World"]
    # => String.capitalize/1: lowercase rest, uppercase first letter
    # => "ELIXIR" → "Elixir" (not "ELIXIR")
    |> Enum.join(" ")
    # => Join with space: ["Hello", "World"] → "Hello World"
    # => Enum.join/2: concatenates list with separator
  end
  # => Returns: capitalized string
  # => Doctests verify: lowercase input, mixed case input

  @doc """
  Checks if string is palindrome.

  ## Examples

      iex> StringHelper.palindrome?("racecar")
      true

      iex> StringHelper.palindrome?("hello")
      false

      iex> StringHelper.palindrome?("A man a plan a canal Panama" |> String.downcase() |> String.replace(" ", ""))
      true
  """
  # => ## Examples: 3 doctests
  # => Test cases: palindrome, non-palindrome, complex palindrome with pipe
  # => Third example: pipeline in doctest (valid syntax)
  def palindrome?(string) do
    # => palindrome?/1: checks if string reads same forwards/backwards
    # => Convention: ? suffix for boolean functions
    string == String.reverse(string)
    # => Compare string with its reverse
    # => "racecar" == "racecar" → true
    # => "hello" == "olleh" → false
    # => Returns: boolean
  end
  # => Doctests verify: true case, false case, preprocessed input
end
# => StringHelper complete: all functions have doctests

defmodule StringHelperTest do
  # => Test module for StringHelper
  use ExUnit.Case
  # => Import ExUnit testing macros
  doctest StringHelper  # Runs all doctests from @doc comments
  # => doctest/1: extracts examples from StringHelper @doc comments
  # => Scans for ## Examples sections
  # => Converts iex> lines to test assertions
  # => Total: 8 doctests (3 + 3 + 2 + 3 from all functions)
  # => Each doctest becomes separate test case
  # => Doctests run alongside regular tests

  # Additional regular tests
  test "reverse handles unicode" do
    # => Regular ExUnit test (not doctest)
    # => Tests edge case not in doctests (unicode)
    assert StringHelper.reverse("Hello 世界") == "界世 olleH"
    # => Verify unicode handling: Chinese characters reversed
    # => "Hello 世界" → "界世 olleH" (character-level reversal)
    # => Doctests + regular tests = comprehensive coverage
  end
  # => Best practice: doctests for basic cases, regular tests for edge cases
end
# => StringHelperTest complete: doctests + unicode test


# Calculator with multi-clause doctests
defmodule Calculator do
  # => Module demonstrating multi-clause functions with doctests
  @doc """
  Performs calculation based on operator.

  ## Examples

      iex> Calculator.calculate(5, :add, 3)
      8

      iex> Calculator.calculate(10, :subtract, 4)
      6

      iex> Calculator.calculate(3, :multiply, 4)
      12

      iex> result = Calculator.calculate(10, :divide, 2)
      iex> result
      5.0

      iex> Calculator.calculate(10, :divide, 0)
      {:error, :division_by_zero}
  """
  # => @doc: single documentation block for all clauses
  # => ## Examples: 5 doctests covering all operations
  # => Fourth example: multi-line doctest (assign then check)
  # => Fifth example: error case (division by zero)
  # => Doctests verify all function clauses work correctly
  def calculate(a, :add, b), do: a + b
  # => First clause: addition operator
  # => Pattern: second arg is :add atom
  # => Returns: sum (integer or float)
  # => Doctest: 5 + 3 = 8 ✓
  def calculate(a, :subtract, b), do: a - b
  # => Second clause: subtraction operator
  # => Pattern: second arg is :subtract atom
  # => Returns: difference
  # => Doctest: 10 - 4 = 6 ✓
  def calculate(a, :multiply, b), do: a * b
  # => Third clause: multiplication operator
  # => Pattern: second arg is :multiply atom
  # => Returns: product
  # => Doctest: 3 * 4 = 12 ✓
  def calculate(_a, :divide, 0), do: {:error, :division_by_zero}
  # => Fourth clause: division by zero (error case)
  # => Pattern: third arg is 0
  # => Matches before general divide clause (clause order matters)
  # => _a: ignore numerator (not needed)
  # => Returns: error tuple (not exception)
  # => Doctest: 10 / 0 = {:error, :division_by_zero} ✓
  def calculate(a, :divide, b), do: a / b
  # => Fifth clause: general division
  # => Pattern: third arg is not 0 (previous clause didn't match)
  # => Returns: quotient (always float in Elixir)
  # => Doctest: 10 / 2 = 5.0 (float, not 5) ✓
end
# => Calculator complete: multi-clause function with comprehensive doctests
# => All 5 clauses tested via doctests
# => Demonstrates: doctests work with pattern matching and multi-clause functions

Key Takeaway: Doctests embed executable examples in @doc comments using iex> prompts. Enable with doctest ModuleName in tests. They keep documentation accurate and provide basic test coverage.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 50: String Manipulation Advanced

Elixir strings are UTF-8 binaries. The String module provides extensive manipulation functions. Understanding binaries, charlists, and Unicode handling is essential for text processing.

Brief Explanation: Strings in Elixir are UTF-8 encoded binaries, not character lists. The String module provides grapheme-aware functions for proper Unicode handling. Key concepts include: binary vs charlist distinction, graphemes (visual characters) vs codepoints (Unicode units), and UTF-8 multibyte character support.

Code:

# Strings are UTF-8 binaries
string = "Hello, 世界!"
# => "Hello, 世界!" (UTF-8 encoded, Unicode support)
is_binary(string)
# => true (strings are binaries, not character lists)

# String length vs byte size
String.length("Hello")
# => 5 (counts graphemes)
String.length("世界")
# => 2 (graphemes, not bytes)
byte_size("世界")
# => 6 (3 bytes per Chinese character in UTF-8)

# Charlists vs Strings
charlist = 'hello'
# => 'hello' (single quotes = list of integers)
is_list(charlist)
# => true
charlist === [104, 101, 108, 108, 111]
# => true (charlist is list of ASCII codes)

# Converting between strings and charlists
String.to_charlist("hello")
# => 'hello' (binary → charlist for Erlang interop)
List.to_string('hello')
# => "hello" (charlist → binary)

# String slicing
String.slice("Hello", 0, 3)
# => "Hel" (start index 0, length 3)
String.slice("Hello", 1..-1)
# => "ello" (range: index 1 to end)
String.slice("Hello", -3, 3)
# => "llo" (negative index from end)

# String character access
String.at("Hello", 1)
# => "e" (0-based index)
String.at("Hello", -1)
# => "o" (negative = from end)

# String searching
String.contains?("Hello World", "World")
# => true (case-sensitive)
String.contains?("Hello World", ["Hi", "Hello"])
# => true (OR logic: any substring matches)
String.starts_with?("Hello", "He")
# => true
String.ends_with?("Hello", "lo")
# => true

# Case conversion
String.upcase("hello")
# => "HELLO"
String.downcase("HELLO")
# => "hello"
String.capitalize("hello world")
# => "Hello world" (only first char)

# Whitespace trimming
String.trim("  hello  ")
# => "hello"
String.trim_leading("  hello  ")
# => "hello  " (trailing preserved)
String.trim_trailing("  hello  ")
# => "  hello" (leading preserved)

# String splitting and joining
String.split("one,two,three", ",")
# => ["one", "two", "three"]
String.split("hello world")
# => ["hello", "world"] (splits on whitespace by default)
Enum.join(["a", "b", "c"], "-")
# => "a-b-c"

# String replacement
String.replace("hello world", "world", "Elixir")
# => "hello Elixir" (replaces all occurrences)
String.replace("aaa", "a", "b")
# => "bbb" (replaces all)
String.replace("aaa", "a", "b", global: false)
# => "baa" (first only)

# String padding
String.pad_leading("42", 5, "0")
# => "00042"
String.pad_trailing("42", 5, "0")
# => "42000"

# Regular expressions
Regex.match?(~r/hello/, "hello world")
# => true
# => ~r/.../ is regex sigil syntax
Regex.match?(~r/\d+/, "abc123")
# => true (matches digits)
# => \d+ matches one or more digits

Regex.scan(~r/\d+/, "abc 123 def 456")
# => [["123"], ["456"]] (all matches)
# => Returns list of all pattern matches in string

Regex.replace(~r/\d/, "Room 123", "X")
# => "Room XXX"
# => Replaces each digit with "X"

~r/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
|> Regex.named_captures("Date: 2024-12-23")
# => %{"year" => "2024", "month" => "12", "day" => "23"}
# => Named capture groups: (?<name>pattern) extracts to map keys

# String to number conversion
String.to_integer("42")
# => 42
String.to_integer("2A", 16)
# => 42 (hex: 2*16 + 10)
String.to_float("3.14")
# => 3.14
Integer.parse("42 units")
# => {42, " units"} (stops at non-digit)
Float.parse("3.14 pi")
# => {3.14, " pi"}

# Graphemes vs Codepoints
String.graphemes("Hello")
# => ["H", "e", "l", "l", "o"]
String.graphemes("👨‍👩‍👧‍👦")
# => ["👨‍👩‍👧‍👦"] (single visual character)

String.codepoints("Hello")
# => ["H", "e", "l", "l", "o"]
String.codepoints("👨‍👩‍👧‍👦")
# => ["👨", "‍", "👩", "‍", "👧", "‍", "👦"] (7 codepoints for family emoji)

# String interpolation
name = "Alice"
"Hello, #{name}!"
# => "Hello, Alice!" (double quotes only)

# String sigils
~s(String with "quotes")
# => "String with \"quotes\"" (lowercase sigil allows interpolation)
~S(No interpolation #{name})
# => "No interpolation \#{name}" (uppercase = literal)
~r/regex/
# => Regex sigil
~w(one two three)
# => ["one", "two", "three"] (word list)

Unicode Handling:

  • Graphemes: User-visible characters (use for string length, iteration)
  • Codepoints: Unicode units (use for Unicode internals, low-level operations)
  • Bytes: UTF-8 encoding size (use for memory/network calculations)

Charlist vs String:

FeatureString (binary)Charlist (list)
Literal"hello"'hello'
TypeBinaryList of integers
UsageModern ElixirErlang interop
FunctionsString moduleList module

Key Takeaway: Strings are UTF-8 binaries with grapheme-aware functions. Use the String module for manipulation, regex for pattern matching, and understand the difference between graphemes (visual characters) and codepoints (Unicode units).

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.

Example 51: GenServer Session Manager (Production Pattern)

GenServer is OTP’s generic server behavior - a process that maintains state and handles synchronous/asynchronous requests. Production systems use GenServer for session storage, caches, connection pools, and stateful services. This example demonstrates a thread-safe session manager with TTL cleanup.

  %% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC, Brown #CA9161
graph TD
    Client1["Client 1"] --> Put["put(key, value)"]
    Client2["Client 2"] --> Get["get(key)"]
    Client3["Client 3"] --> Delete["delete(key)"]

    Put --> GenServer["GenServer Process<br/>State: %{sessions: map, ttl: 300}"]
    Get --> GenServer
    Delete --> GenServer

    GenServer --> Handle["handle_call/cast<br/>Thread-safe operations"]
    Handle --> State["Updated State"]

    Timer["Periodic Cleanup<br/>:cleanup_expired"] --> GenServer

    style Client1 fill:#0173B2,color:#fff
    style Client2 fill:#0173B2,color:#fff
    style Client3 fill:#0173B2,color:#fff
    style Put fill:#DE8F05,color:#fff
    style Get fill:#DE8F05,color:#fff
    style Delete fill:#DE8F05,color:#fff
    style GenServer fill:#029E73,color:#fff
    style Handle fill:#CC78BC,color:#fff
    style State fill:#CA9161,color:#fff
    style Timer fill:#CC78BC,color:#fff

Code:

defmodule SessionManager do
  # => GenServer-based session manager module
  # => Demonstrates production pattern for stateful services
  @moduledoc """
  GenServer-based session manager with TTL-based cleanup.
  Provides thread-safe concurrent access to session data.
  """
  # => @moduledoc: documentation for module
  # => Explains purpose: session storage with TTL

  use GenServer  # => Use GenServer behavior (implements init, handle_call, handle_cast, etc.)
  # => use GenServer: imports GenServer behavior macros
  # => Requires implementation of: init/1, handle_call/3, handle_cast/2, handle_info/2
  # => Provides: start_link/1, call/2, cast/2, etc.

  # Client API - public interface
  # => Client API: functions callers use (abstracts GenServer protocol)
  # => Best practice: separate client API from server callbacks

  @doc "Starts the session manager with optional TTL (default: 300 seconds)"
  def start_link(opts \\ []) do
    # => start_link/1: starts GenServer process
    # => opts: keyword list of options
    # => Default: [] (empty)
    ttl = Keyword.get(opts, :ttl, 300)  # => Default 5 minutes
    # => Keyword.get/3: extract :ttl from opts, default 300
    # => ttl: time-to-live in seconds
    GenServer.start_link(__MODULE__, ttl, name: __MODULE__)
    # => GenServer.start_link/3: spawns GenServer process
    # => First arg: module name (__MODULE__ = SessionManager)
    # => Second arg: init arg (ttl passed to init/1)
    # => Third arg: options (name: registers process globally)
    # => name: __MODULE__: allows calling SessionManager.put/2 without PID
    # => Returns: {:ok, pid}
    # => __MODULE__ = SessionManager, name registers process globally
  end

  @doc "Stores a session with given key and value"
  def put(key, value) do
    # => put/2: client function to store session
    # => key: session identifier (string or atom)
    # => value: session data (any Elixir term)
    GenServer.call(__MODULE__, {:put, key, value})  # => Synchronous call
    # => GenServer.call/2: sends synchronous request
    # => __MODULE__: named process (SessionManager)
    # => Message: {:put, key, value}
    # => Blocks until handle_call returns reply
    # => Returns: :ok (from handle_call reply)
    # => Blocks until server responds
    # => Thread-safe: only one caller modifies state at a time
  end

  @doc "Retrieves session by key, returns {:ok, value} or {:error, :not_found}"
  def get(key) do
    # => get/1: client function to retrieve session
    # => key: session identifier
    GenServer.call(__MODULE__, {:get, key})  # => Synchronous call
    # => Message: {:get, key}
    # => Blocks until handle_call returns
    # => Returns: {:ok, value} or {:error, :not_found} or {:error, :expired}
  end

  @doc "Deletes session by key"
  def delete(key) do
    # => delete/1: client function to remove session
    # => key: session identifier
    GenServer.cast(__MODULE__, {:delete, key})  # => Asynchronous cast
    # => GenServer.cast/2: sends asynchronous request (fire-and-forget)
    # => Message: {:delete, key}
    # => Returns immediately :ok (doesn't wait for processing)
    # => handle_cast processes message asynchronously
    # => Returns immediately without waiting for completion
    # => Use cast when reply not needed (performance optimization)
  end

  @doc "Returns all active sessions (for debugging)"
  def list_all do
    # => list_all/0: retrieves all active sessions
    # => Debugging function (not for production use at scale)
    GenServer.call(__MODULE__, :list_all)  # => Synchronous call
    # => Message: :list_all (atom, not tuple)
    # => Returns: map of %{key => value} (without timestamps)
  end

  @doc "Returns session count"
  def count do
    # => count/0: returns number of active sessions
    GenServer.call(__MODULE__, :count)  # => Synchronous call
    # => Message: :count
    # => Returns: integer count
  end

  # Server Callbacks - GenServer implementation
  # => Server callbacks: handle requests and manage state
  # => Private to module (not called directly by clients)

  @impl true
  # => @impl true: marks function as behavior callback implementation
  # => Compiler verifies function matches GenServer behavior contract
  def init(ttl) do
    # => init/1: GenServer callback for initialization
    # => Called when GenServer.start_link/3 executes
    # => Arg: ttl (from start_link second argument)
    # Called when GenServer starts
    # Initialize state with empty sessions map and TTL
    state = %{
      # => State: map with sessions and ttl
      # => Immutable: each callback returns new state
      sessions: %{},  # => key -> {value, inserted_at}
      # => sessions: map of session_key => {session_value, timestamp}
      # => Empty on initialization
      ttl: ttl        # => Time-to-live in seconds
      # => ttl: expiration duration (e.g., 300 seconds)
    }
    # => state is %{sessions: %{}, ttl: 300}

    # Schedule periodic cleanup every 60 seconds
    schedule_cleanup()  # => Sends message to self after delay
    # => schedule_cleanup/0: schedules first :cleanup_expired message
    # => Process.send_after/3 sends message in 60 seconds
    # => Recursive pattern: cleanup schedules next cleanup

    {:ok, state}  # => Return {:ok, initial_state}
    # => init/1 contract: returns {:ok, state}
    # => GenServer.start_link/3 returns {:ok, pid}
    # => Process now running with state
  end

  @impl true
  def handle_call({:put, key, value}, _from, state) do
    # => handle_call/3: callback for synchronous requests
    # => First arg: request message {:put, key, value}
    # => Second arg: _from = {caller_pid, unique_ref} (unused here)
    # => Third arg: current state
    # => Synchronous request handler
    # _from = {pid, ref} of caller (unused here)

    # Store session with current timestamp
    session_data = {value, System.system_time(:second)}
    # => session_data: tuple {session_value, unix_timestamp}
    # => System.system_time(:second): current time in seconds
    # => Example: {"Alice", 1734567890}
    updated_sessions = Map.put(state.sessions, key, session_data)
    # => Map.put/3: add/update key-value in map
    # => state.sessions: old sessions map
    # => key: session key (e.g., "user_123")
    # => session_data: {value, timestamp}
    # => Returns: new sessions map with added entry

    new_state = %{state | sessions: updated_sessions}  # => Update state
    # => Map update syntax: %{old_map | key: new_value}
    # => Replaces sessions field, keeps ttl unchanged
    # => new_state is %{sessions: updated_sessions, ttl: state.ttl}

    {:reply, :ok, new_state}  # => Reply to caller with :ok, update state
    # => handle_call contract: {:reply, reply_value, new_state}
    # => reply_value: :ok (sent to caller)
    # => new_state: becomes current state
    # => GenServer.call returns :ok to caller
  end

  @impl true
  def handle_call({:get, key}, _from, state) do
    # => handle_call/3: callback for get request
    # => Message: {:get, key}
    # Synchronous request handler for get

    case Map.get(state.sessions, key) do
      # => Map.get/2: retrieves value by key (returns nil if not found)
      # => state.sessions: current sessions map
      # => Pattern matching on result
      nil ->
        # => Pattern 1: key not found in map
        {:reply, {:error, :not_found}, state}  # => Session doesn't exist
        # => Reply: {:error, :not_found}
        # => State unchanged (read operation)
        # => Caller gets {:error, :not_found}

      {value, inserted_at} ->
        # => Pattern 2: found {value, timestamp} tuple
        # => value: session data
        # => inserted_at: timestamp when stored
        # Check if session expired
        current_time = System.system_time(:second)
        # => Current time in seconds
        age = current_time - inserted_at
        # => age: seconds since session created
        # => Example: 1734567900 - 1734567890 = 10 seconds old

        if age > state.ttl do
          # => Check if session older than TTL
          # => Example: 10 > 300 (false), 400 > 300 (true)
          # Session expired - remove it
          updated_sessions = Map.delete(state.sessions, key)
          # => Map.delete/2: removes key from map
          # => Cleanup expired session on read
          new_state = %{state | sessions: updated_sessions}
          # => Update state without expired session
          {:reply, {:error, :expired}, new_state}
          # => Reply: {:error, :expired}
          # => State updated (session removed)
          # => Lazy cleanup: remove on read instead of waiting for periodic cleanup
        else
          # Session valid - return value
          {:reply, {:ok, value}, state}  # => State unchanged
          # => Reply: {:ok, session_value}
          # => State unchanged (session still valid)
          # => Caller gets {:ok, value}
        end
    end
  end

  @impl true
  def handle_call(:list_all, _from, state) do
    # => handle_call/3: callback for list_all request
    # => Message: :list_all (atom)
    # Return all sessions (for debugging)
    sessions = state.sessions
               |> Enum.map(fn {key, {value, _ts}} -> {key, value} end)
               # => Transform each {key, {value, timestamp}} to {key, value}
               # => Removes timestamps from output
               # => Example: {"user_123", {"Alice", 1734567890}} → {"user_123", "Alice"}
               |> Enum.into(%{})
               # => Convert list of tuples back to map
               # => Returns: %{key1 => value1, key2 => value2}

    {:reply, sessions, state}  # => Return map without timestamps
    # => Reply: map of sessions (no timestamps)
    # => State unchanged
  end

  @impl true
  def handle_call(:count, _from, state) do
    # => handle_call/3: callback for count request
    # Return session count
    count = map_size(state.sessions)
    # => map_size/1: returns number of keys in map
    # => Example: %{"user_123" => ..., "user_456" => ...} → 2
    {:reply, count, state}
    # => Reply: integer count
    # => State unchanged
  end

  @impl true
  def handle_cast({:delete, key}, state) do
    # => handle_cast/2: callback for asynchronous requests
    # => First arg: request message {:delete, key}
    # => Second arg: current state
    # => No _from (async, no reply)
    # Asynchronous request handler (no reply sent)
    updated_sessions = Map.delete(state.sessions, key)
    # => Remove key from sessions map
    new_state = %{state | sessions: updated_sessions}
    # => Update state with session removed

    {:noreply, new_state}  # => No reply for cast, just update state
    # => handle_cast contract: {:noreply, new_state}
    # => No reply sent to caller
    # => State updated
    # => Caller already received :ok from GenServer.cast
  end

  @impl true
  def handle_info(:cleanup_expired, state) do
    # => handle_info/2: callback for arbitrary messages (not call/cast)
    # => First arg: message (:cleanup_expired atom)
    # => Second arg: current state
    # => Handles messages from Process.send_after, timers, monitors, etc.
    # Handle messages sent to process (not call/cast)
    # Cleanup expired sessions

    current_time = System.system_time(:second)
    # => Current time for age calculation

    # Filter out expired sessions
    active_sessions = state.sessions
                      |> Enum.filter(fn {_key, {_value, inserted_at}} ->
                        # => Filter predicate: returns true to keep, false to remove
                        # => Pattern: {key, {value, timestamp}}
                        # => _key, _value: ignored (not needed for age check)
                        age = current_time - inserted_at
                        # => Calculate session age
                        age <= state.ttl  # => Keep only non-expired
                        # => true if age <= ttl (keep), false if age > ttl (remove)
                        # => Example: 295 <= 300 (true, keep), 305 <= 300 (false, remove)
                      end)
                      |> Enum.into(%{})
                      # => Convert filtered list back to map
                      # => active_sessions: map of non-expired sessions

    removed_count = map_size(state.sessions) - map_size(active_sessions)
    # => Calculate how many sessions expired
    # => Before size - after size = removed count
    if removed_count > 0 do
      # => Only log if sessions were removed
      IO.puts("Cleaned up #{removed_count} expired sessions")
      # => Log cleanup activity
      # => Production: use Logger.info instead
    end

    new_state = %{state | sessions: active_sessions}
    # => Update state with only active sessions

    # Schedule next cleanup
    schedule_cleanup()
    # => Recursive scheduling: cleanup schedules next cleanup
    # => Ensures periodic cleanup continues
    # => Pattern: self-scheduling timer

    {:noreply, new_state}  # => Update state, no reply
    # => handle_info contract: {:noreply, new_state}
    # => No caller to reply to (message from timer)
    # => State updated with cleaned sessions
  end

  # Private helpers
  # => Private functions: not part of public API

  defp schedule_cleanup do
    # => schedule_cleanup/0: schedules next cleanup message
    # => defp: private function
    # Send :cleanup_expired message to self after 60 seconds
    Process.send_after(self(), :cleanup_expired, 60_000)  # => 60 seconds
    # => Process.send_after/3: sends message after delay
    # => self(): current GenServer process PID
    # => Message: :cleanup_expired atom
    # => Delay: 60_000ms = 60 seconds
    # => Returns: timer reference (unused here)
    # => Message handled by handle_info(:cleanup_expired, state)
  end
end
# => SessionManager module complete

# Usage examples
{:ok, _pid} = SessionManager.start_link(ttl: 10)  # => 10 second TTL for demo
# => Start GenServer with 10 second TTL (for quick demo)
# => Returns: {:ok, pid}
# => _pid: ignore PID (process registered by name)
# => GenServer now running with state %{sessions: %{}, ttl: 10}

SessionManager.put("user_123", %{name: "Alice", role: :admin})
# => Store session for user_123
# => Value: map with name and role
# => Synchronous call, waits for :ok reply
# => State now: %{sessions: %{"user_123" => {%{name: "Alice", role: :admin}, timestamp}}, ttl: 10}
SessionManager.put("user_456", %{name: "Bob", role: :user})
# => Store session for user_456
# => State: 2 sessions stored
SessionManager.put("user_789", %{name: "Charlie", role: :guest})
# => Store session for user_789
# => State: 3 sessions stored

SessionManager.get("user_123")  # => {:ok, %{name: "Alice", role: :admin}}
# => Retrieve existing session
# => Synchronous call
# => Returns: {:ok, session_value}
# => Session still valid (recently created)
SessionManager.get("user_999")  # => {:error, :not_found}
# => Try to retrieve non-existent session
# => Returns: {:error, :not_found}
# => Key doesn't exist in sessions map

SessionManager.delete("user_456")
# => Delete user_456 session
# => Asynchronous cast (fire-and-forget)
# => Returns immediately: :ok
# => Session removed from state
SessionManager.get("user_456")  # => {:error, :not_found}
# => Try to retrieve deleted session
# => Returns: {:error, :not_found}
# => Session no longer exists

SessionManager.list_all()  # => %{"user_123" => %{...}, "user_789" => %{...}}
# => Retrieve all active sessions
# => Returns: map of {key => value} (no timestamps)
# => user_456 not present (was deleted)
# => user_123 and user_789 present
SessionManager.count()  # => 2
# => Count active sessions
# => Returns: 2 (user_123 and user_789)

:timer.sleep(11_000)  # => 11 seconds
# => Sleep for 11 seconds
# => Simulates time passing
# => Sessions now 11 seconds old (older than 10 second TTL)
SessionManager.get("user_123")  # => {:error, :expired}
# => Try to retrieve expired session
# => age = 11 seconds, ttl = 10 seconds
# => 11 > 10: session expired
# => handle_call removes expired session and returns {:error, :expired}
# => Demonstrates TTL expiration

Key Takeaway: GenServer provides thread-safe stateful processes via callbacks. Use GenServer.call/2 for synchronous requests that need replies, GenServer.cast/2 for asynchronous fire-and-forget operations. State is immutable - callbacks return new state. handle_info/2 handles arbitrary messages like timers. GenServer processes run concurrently and isolate state, enabling millions of concurrent sessions.

Why It Matters: GenServer is the foundation of OTP applications. It handles concurrency, state management, and fault tolerance automatically. Production Elixir systems use GenServer for caches, connection pools, rate limiters, session stores, and background workers. Understanding the GenServer pattern (client API calling server callbacks that update state) is essential for building scalable, concurrent systems. Add GenServers to supervision trees for automatic restart on crashes.


Example 52: Supervisor Child Specifications

Supervisors define child processes using child specifications that control restart behavior, shutdown timeouts, and process types. Understanding child specs enables fine-grained control over supervision trees.

Child Specification Structure

FieldTypePurposeCommon Values
:idterm()Unique identifier within supervisorModule name, atom, tuple
:start{m, f, a}MFA tuple to start child{MyWorker, :start_link, [opts]}
:restartatom()When to restart child:permanent, :transient, :temporary
:shutdowntimeout()Graceful shutdown duration5000 (ms), :infinity, :brutal_kill
:typeatom()Process type:worker, :supervisor

Restart Strategies

:permanent - Always restart (critical services)

  • Use for: database connections, core services, state machines
  • Behavior: Any exit (normal or crash) triggers restart

:transient - Restart only on abnormal exit

  • Use for: tasks that complete successfully, retryable operations
  • Behavior: Normal exit (:normal, :shutdown) → no restart; crash → restart

:temporary - Never restart (one-time tasks)

  • Use for: fire-and-forget operations, disposable workers
  • Behavior: Any exit → remove from supervision tree

Code:

# Basic child specification map
child_spec = %{
  # => Child spec: defines how supervisor manages process lifecycle
  id: MyWorker,
  # => Unique identifier within supervisor
  start: {MyWorker, :start_link, [[name: :worker_1]]},
  # => MFA tuple: supervisor calls MyWorker.start_link([name: :worker_1])
  # => Must return {:ok, pid} or {:ok, pid, info}
  restart: :permanent,
  # => :permanent - always restart on exit (critical processes)
  # => :transient - restart only on abnormal exit
  # => :temporary - never restart (one-time tasks)
  shutdown: 5000,
  # => Graceful shutdown timeout: 5000ms
  # => After timeout, supervisor sends :kill signal
  # => :brutal_kill - immediate kill, :infinity - wait forever
  type: :worker
  # => :worker - leaf process, :supervisor - nested supervisor
}
# => Returns: child spec map for Supervisor.start_link/2

# Worker module implementing child_spec/1
defmodule MyWorker do
  # => Worker with custom child_spec/1 for flexible configuration
  use GenServer
  # => Imports default child_spec/1 implementation

  def start_link(opts) do
    # => Starts GenServer linked to caller
    name = Keyword.get(opts, :name, __MODULE__)
    # => Extract :name from opts, default to module name
    GenServer.start_link(__MODULE__, opts, name: name)
    # => Start GenServer and register with name
    # => Returns: {:ok, pid}
  end

  # Override default child spec
  def child_spec(opts) do
    # => Custom child specification callback
    # => Called by supervisor when starting child
    %{
      id: Keyword.get(opts, :name, __MODULE__),
      # => Dynamic id from opts enables multiple instances
      # => Each instance needs unique id in supervisor
      start: {__MODULE__, :start_link, [opts]},
      # => Pass opts through to start_link
      restart: :permanent,
      # => Always restart on exit (critical worker)
      shutdown: 10_000
      # => 10s timeout for cleanup (flush queues, close connections)
    }
    # => Returns: custom child spec map
  end

  @impl true
  def init(opts), do: {:ok, opts}
  # => Store opts as GenServer state
end
# => MyWorker complete

# Critical worker example (permanent restart)
defmodule Database do
  # => Database worker: critical service with permanent restart
  use GenServer

  def child_spec(_opts) do
    # => Custom child spec for database process
    %{
      id: __MODULE__,
      # => Single instance (module name as id)
      start: {__MODULE__, :start_link, []},
      # => No configuration needed
      restart: :permanent,
      # => Critical: database must always run
      # => Any exit triggers immediate restart
      shutdown: 30_000
      # => Long timeout: flush writes, close connections, release locks
      # => After 30s, supervisor sends :kill (may lose data)
    }
    # => Returns: child spec with critical worker settings
  end

  def start_link, do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
  # => Start and register as Database

  @impl true
  def init(_), do: {:ok, %{}}
  # => Initialize empty state (real DB would connect here)
end
# => Database complete

# Optional service example (transient restart)
defmodule Cache do
  # => Cache worker: transient restart (crash → restart, normal exit → remove)
  use GenServer

  def child_spec(_opts) do
    # => Custom child spec for cache process
    %{
      id: __MODULE__,
      # => Single instance
      start: {__MODULE__, :start_link, []},
      # => Self-contained cache (no configuration)
      restart: :transient,
      # => Restart on crash only
      # => Normal exit (cache clears): no restart
      # => Abnormal exit (bug, OOM): restart
      shutdown: 5_000
      # => Standard timeout (in-memory data flushes quickly)
    }
    # => Returns: child spec with transient restart
  end

  def start_link, do: GenServer.start_link(__MODULE__, [], name: __MODULE__)
  # => Start and register as Cache

  @impl true
  def init(_), do: {:ok, %{}}
  # => Initialize empty cache
end
# => Cache complete

# Supervisor with multiple children
defmodule MyApp.Supervisor do
  # => Application supervisor managing multiple children
  use Supervisor

  def start_link(opts) do
    # => Start supervisor process
    Supervisor.start_link(__MODULE__, opts, name: __MODULE__)
    # => Calls init/1 callback with opts
    # => Returns: {:ok, pid}
  end

  @impl true
  def init(_opts) do
    # => Supervisor callback: define children and strategy
    children = [
      # => Children started in order (top to bottom)
      Database,
      # => Module form: calls Database.child_spec([])
      # => Shorthand for {Database, []}
      Cache,
      # => Cache started after Database (dependency order)
      {MyWorker, name: :worker_1},
      # => Tuple form: {module, opts}
      # => Calls MyWorker.child_spec([name: :worker_1])
      # => id: :worker_1 from opts
      {MyWorker, name: :worker_2}
      # => Second instance with different id (no conflict)
    ]
    # => 4 children: Database, Cache, worker_1, worker_2

    Supervisor.init(children, strategy: :one_for_one)
    # => :one_for_one: restart only failed child (not siblings)
    # => Other strategies: :one_for_all, :rest_for_one
    # => Returns: {:ok, {supervisor_flags, children}}
  end
end
# => MyApp.Supervisor complete

# Starting the supervisor
{:ok, sup_pid} = MyApp.Supervisor.start_link([])
# => Supervisor starts all 4 children in order
# => Database → Cache → worker_1 → worker_2
# => Returns: {:ok, pid} where pid is supervisor process

Supervisor.which_children(sup_pid)
# => Lists all children: [{id, pid, type, modules}, ...]
# => Example: [{Database, #PID<0.200.0>, :worker, [Database]}, ...]

Supervisor.count_children(sup_pid)
# => Returns: %{active: 4, specs: 4, supervisors: 0, workers: 4}
# => active: running children, specs: total child specs

Key Takeaway: Child specs control how supervisors manage children. Use :permanent for critical processes, :transient for expected failures, :temporary for one-time tasks. Implement child_spec/1 to customize restart and shutdown behavior.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 53: Application Callbacks and Lifecycle

Application behavior defines callbacks for application startup and shutdown. Implement start/2 to initialize supervision trees and stop/1 for cleanup.

Brief Explanation: The Application behavior manages application lifecycle through callbacks. start/2 initializes the application (typically starts a supervision tree), receives startup type and arguments. stop/1 handles graceful shutdown (cleanup resources). Applications are OTP’s top-level abstraction for managing related processes and resources.

Code:

defmodule MyApp.Application do
  # => Application module (conventionally: AppName.Application)
  use Application
  # => Implements Application behavior (requires start/2)

  @impl true
  def start(_type, _args) do
    # => start/2: called when application starts
    # => _type: startup type (:normal, :takeover, :failover)
    # => _args: application arguments from config
    # => Returns: {:ok, pid} or {:error, reason}

    children = [
      # => Child specifications for supervision tree
      {MyApp.Repo, []},
      # => Database connection pool
      {MyApp.Endpoint, []},
      # => Phoenix web server endpoint
      {MyApp.Worker, []}
      # => Custom worker process
    ]
    # => List of child specs

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    # => Supervisor options
    # => strategy: :one_for_one (restart only failed child)
    # => name: registered name for supervisor

    Supervisor.start_link(children, opts)
    # => Starts supervision tree
    # => Returns: {:ok, supervisor_pid}
  end

  @impl true
  def stop(_state) do
    # => stop/1: called before application stops
    # => _state: state returned from start/2 (usually ignored)
    # => Cleanup resources (close connections, flush buffers)
    :ok
    # => Must return :ok
  end
end

# config/config.exs
# use MyApp.Application, otp_app: :my_app
# => Registers MyApp.Application as application module
# => OTP starts this module during boot

Application Lifecycle:

PhaseCallbackPurposeReturn
Startupstart/2Initialize processes{:ok, pid}
Running-Supervision tree active-
Shutdownstop/1Cleanup resources:ok

Startup Types:

  • :normal - Standard application start
  • :takeover - Taking over from another node (distributed)
  • :failover - Failover from failed node (distributed)

Best Practices:

  • Keep start/2 fast: Heavy initialization in child processes
  • Return supervision tree PID: Application monitors top supervisor
  • Cleanup in stop/1: Close connections, flush logs, release resources
  • Use child specs: Leverage Supervisor for process management

Key Takeaway: Applications implement start/2 to initialize supervision trees and stop/1 for cleanup. The Application behavior is OTP’s top-level abstraction for managing related processes. Return {:ok, supervisor_pid} from start/2, :ok from stop/1.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.

Example 54: Custom Mix Tasks

Mix tasks automate project operations. Create custom tasks by implementing Mix.Task behavior with run/1 function.

Brief Explanation: Custom Mix tasks extend Mix’s functionality for project-specific automation. Tasks implement the Mix.Task behavior with a run/1 entry point that receives command-line arguments. Use OptionParser.parse/2 for argument parsing, Mix.shell().info/1 for output, and module attributes (@shortdoc, @moduledoc) for documentation.

Code:

defmodule Mix.Tasks.Hello do
  # => Custom Mix task: mix hello
  # => Module name: Mix.Tasks.TaskName (CamelCase)
  use Mix.Task
  # => Implements Mix.Task behavior (requires run/1)
  # => Imports task infrastructure

  @shortdoc "Prints hello message"
  # => Short description (shown in mix help)
  # => Displayed in task list

  @moduledoc """
  Greets the user with a hello message.

  ## Usage
      mix hello
      mix hello --name Alice
  """
  # => Full documentation (shown in mix help hello)
  # => Markdown format supported

  @impl Mix.Task
  def run(args) do
    # => run/1: task entry point
    # => args: list of command-line arguments (strings)
    {opts, _, _} = OptionParser.parse(args, switches: [name: :string])
    # => Parses args: --name Alice → [name: "Alice"]
    # => opts: keyword list of parsed options
    # => switches: expected option types

    name = opts[:name] || "World"
    # => Gets name option with default fallback
    # => opts[:name]: nil if not provided

    Mix.shell().info("Hello, #{name}!")
    # => Output to console via Mix shell
    # => Output: "Hello, World!" or "Hello, Alice!"
    # => Mix.shell(): current shell IO interface
  end
end

# Usage: mix hello --name Alice
# => Runs custom task from command line
# => Output: "Hello, Alice!"

Mix Task Anatomy:

ComponentPurposeRequired
use Mix.TaskImports behaviorYes
@shortdocBrief description (mix help)Recommended
@moduledocFull documentationRecommended
run/1Entry point (receives args)Yes
@impl Mix.TaskExplicit behavior implementationBest practice

Argument Parsing:

# OptionParser.parse(args, switches: [...])
{opts, positional, invalid} = OptionParser.parse(  # => Parses command-line arguments
  args,                                              # => Input args from Mix.Task.run/1
  switches: [                                        # => Define expected switches/options
    name: :string,    # --name Alice                # => String option with value
    count: :integer,  # --count 5                   # => Integer option (auto-parsed)
    verbose: :boolean # --verbose                   # => Boolean flag (presence = true)
  ]
)
# => Returns 3-element tuple: {parsed_opts, positional_args, invalid_opts}
# => opts: [name: "Alice", count: 5, verbose: true]
# => positional: unnamed arguments list (e.g., ["file1.txt", "file2.txt"])
# => invalid: unrecognized options (e.g., [{"-x", nil}] for --unknown-flag)

Key Takeaway: Custom Mix tasks automate project operations. Implement Mix.Task behavior with run/1, parse arguments with OptionParser, and document with @shortdoc/@moduledoc. Run tasks with mix task_name [args].

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.

Example 55: Runtime Configuration

Runtime configuration loads settings when the application starts (not compile time). Use config/runtime.exs for environment variables and production secrets.

Brief Explanation: Runtime configuration (config/runtime.exs) loads settings at application startup, enabling environment-specific configuration and secrets management. Unlike config/config.exs (compile-time), runtime config values aren’t baked into releases, making it safe for secrets. Use Application.get_env/2 to read config, Application.fetch_env!/2 for required values.

Code:

# config/runtime.exs - runs at application startup
# => Executed when application starts (not at compile time)
import Config
# => Required for config/2 macro
# => Imports Config.config/2 for configuration DSL

config :my_app,
  # => Configures application :my_app
  # => First arg: application atom, second arg: keyword list
  secret_key: System.get_env("SECRET_KEY") || raise("SECRET_KEY not set"),
  # => Reads SECRET_KEY env var, raises if missing (required config)
  # => System.get_env/1 returns string or nil
  # => || raise/1: fail-fast pattern for required secrets
  database_url: System.get_env("DATABASE_URL") || raise("DATABASE_URL not set"),
  # => Database connection string, required in production
  # => Raises if DATABASE_URL env var not set
  port: String.to_integer(System.get_env("PORT") || "4000")
  # => HTTP port, defaults to 4000, converts string → integer
  # => Environment variables are strings: "4000" → 4000
  # => String.to_integer/1 converts to integer type

if config_env() == :prod do
  # => Production-only config
  # => config_env/0 returns current Mix environment (:dev, :test, :prod)
  # => Guards production-specific settings
  config :my_app, MyApp.Repo,
    # => Ecto repo configuration
    # => Second arg is module name (MyApp.Repo)
    url: System.get_env("DATABASE_URL"),
    # => Database URL
    # => Overrides or extends :my_app config for MyApp.Repo
    pool_size: String.to_integer(System.get_env("POOL_SIZE") || "10")
    # => Connection pool size, defaults to 10
    # => String env var converted to integer
end
# => Config only applied if config_env() == :prod

# Application reads runtime config
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    # => Application startup callback
    port = Application.get_env(:my_app, :port)
    # => Reads port from runtime config
    secret = Application.get_env(:my_app, :secret_key)
    # => Reads secret_key from runtime config

    IO.puts("Starting on port #{port}")
    # => Output: "Starting on port 4000"

    children = [
      {MyApp.Server, port: port, secret: secret}
      # => Passes runtime config to child process
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
    # => Starts supervision tree
  end
end

# Config accessor module (best practice)
defmodule MyApp.Config do
  # => Centralizes config access with defaults

  def port, do: Application.get_env(:my_app, :port, 4000)
  # => Returns port with default 4000

  def secret_key, do: Application.fetch_env!(:my_app, :secret_key)
  # => Returns secret_key, raises if missing (required config)

  def database_url, do: Application.fetch_env!(:my_app, :database_url)
  # => Returns database_url, raises if missing

  def timeout, do: Application.get_env(:my_app, :timeout, 5000)
  # => Returns timeout with default 5000ms
end

Compile-Time vs Runtime Config:

Featureconfig/config.exs (Compile-Time)config/runtime.exs (Runtime)
ExecutionMix compileApplication startup
ValuesBaked into releaseLoaded from environment
Use CaseDefaults, non-secretsSecrets, env variables
Security❌ Don’t use for secrets✅ Safe for secrets

Best Practices:

  • Required config: Use || raise() or fetch_env!/2 (fail fast)
  • Optional config: Use get_env/3 with defaults
  • Type conversion: Environment variables are always strings
  • Config accessor: Create module for centralized access (e.g., MyApp.Config)

Key Takeaway: Use config/runtime.exs for secrets and environment-specific configuration. Runtime config loads at startup (not compile time), making it safe for production secrets. Use Application.get_env/2 for optional config, Application.fetch_env!/2 for required config.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.

Example 56: Process Links and Crash Propagation

Linked processes crash together—when one exits abnormally, linked processes receive exit signals. Use linking for tightly-coupled processes that should fail together.

Brief Explanation: Links create bidirectional connections between processes for crash propagation. When a linked process crashes, all connected processes receive exit signals and crash too (unless trapping exits). This pattern enables supervisor-like behavior where parent processes detect and handle child crashes. The :trap_exit flag converts exit signals into messages, preventing crash propagation.

Code:

parent = self()
# => Current process PID
# => Type: pid()

child = spawn_link(fn ->
  # => spawn_link/1: spawns process AND links it to caller
  # => Creates bidirectional link (crash propagation enabled)
  :timer.sleep(1000)
  # => Sleep 1 second before crashing
  raise "Child crashed!"
  # => Child exits abnormally → parent crashes too
  # => Exit signal propagates via link
end)
# => Returns child PID, linked bidirectionally
# => child: pid()

Process.alive?(child)
# => true (child still sleeping)
:timer.sleep(1500)
# => Wait for child to crash (after 1 second)
Process.alive?(child)
# => false (child crashed after 1 second)
# Parent process also crashed due to link!

# Manual linking with Process.link/1
pid1 = spawn(fn -> :timer.sleep(10_000) end)
# => Spawn process (NOT linked yet)
# => Independent process, sleeps 10 seconds
pid2 = spawn(fn -> :timer.sleep(10_000) end)
# => Second independent process
# => Also sleeps 10 seconds

Process.link(pid1)
# => Links current process to pid1 bidirectionally
# => Current crashes → pid1 crashes; pid1 crashes → current crashes
Process.link(pid2)
# => Links current process to pid2
# => Triangle: current ↔ pid1, current ↔ pid2 (no direct pid1 ↔ pid2)

Process.exit(pid1, :kill)
# => Kills pid1 with :kill reason
# => Exit signal propagates to current process
# => Current process crashes (unless trapping exits)

# Trap exits to handle crashes gracefully
Process.flag(:trap_exit, true)
# => Converts exit signals to {:EXIT, pid, reason} messages
# => Process won't crash when linked process exits
# => Returns: false (previous trap_exit value)

linked_pid = spawn_link(fn ->
  # => Spawn and link child
  :timer.sleep(500)
  # => Sleep 0.5 seconds
  raise "Linked process error!"
  # => Child crashes with RuntimeError
  # => Exit signal → message (parent trapping exits)
end)
# => Returns child PID

receive do
  # => Wait for exit message
  {:EXIT, ^linked_pid, reason} ->
    # => Pattern match exit message
    # => ^linked_pid: pin operator (match exact PID)
    IO.puts("Linked process exited with reason: #{inspect(reason)}")
    # => Output: "Linked process exited with reason: {%RuntimeError{...}, ...}"
    # => Parent continues running (handled gracefully)
end
# => receive complete
# => Parent process still alive

# Supervisor pattern uses links
defmodule Worker do
  # => GenServer worker for supervisor example
  use GenServer

  def start_link(id) do
    # => Starts worker linked to caller (supervisor)
    GenServer.start_link(__MODULE__, id)
    # => Returns: {:ok, pid}
  end

  @impl true
  def init(id) do
    # => GenServer initialization callback
    IO.puts("Worker #{id} started")
    # => Output: "Worker 1 started"
    {:ok, id}
    # => Returns: {:ok, state}
  end

  @impl true
  def handle_cast(:crash, _state) do
    # => Handles :crash message
    raise "Worker crashed!"
    # => Worker exits abnormally
    # => Supervisor receives {:EXIT, worker_pid, reason}
    # => Supervisor restarts worker per restart strategy
  end
end

# Exit reasons determine propagation behavior
# :normal - graceful exit (doesn't crash linked processes)
spawn_link(fn -> exit(:normal) end)
# => Exits normally
# => Won't crash parent
# => Parent receives {:EXIT, pid, :normal} if trapping

# :kill - forceful kill (cannot be trapped)
# spawn_link(fn -> exit(:kill) end)
# => Exits with :kill
# => Crashes parent immediately
# => trap_exit ignored for :kill

# any other - abnormal exit (crashes linked unless trapping)
spawn_link(fn -> exit(:abnormal) end)
# => Exits abnormally
# => Crashes parent unless trapping exits
# => If trapping: {:EXIT, pid, :abnormal} message

# Unlinking processes
Process.unlink(pid1)
# => Removes bidirectional link to pid1
# => Crashes no longer propagate
# => Returns: true

Exit Reason Behavior:

  • :normal - Graceful exit, doesn’t crash linked processes (worker completed successfully)
  • :kill - Forceful termination, cannot be trapped (emergency shutdown)
  • other - Abnormal exit, crashes linked processes unless trapping

Linking vs Monitoring:

  • Linking: Bidirectional crash propagation (fail-together pattern for supervisor)
  • Monitoring: Unidirectional crash detection (observer pattern for notifications)

Key Takeaway: Linked processes crash together. Use spawn_link/1 for coupled processes. Trap exits with Process.flag(:trap_exit, true) to handle crashes gracefully. Supervisors use links to detect worker crashes.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.

Example 57: Message Mailbox Management

Process mailboxes queue incoming messages. Understanding mailbox behavior prevents memory leaks and enables selective message processing.

Brief Explanation: Every process has a mailbox that queues messages in FIFO order. Messages accumulate until processed by receive blocks. Selective receive pattern-matches messages, potentially scanning the entire mailbox. Monitor mailbox size with Process.info/2 to detect message buildup and prevent memory leaks.

Code:

# Messages accumulate in mailbox
pid = self()
# => Current process PID

send(pid, :msg1)
# => Adds :msg1 to mailbox (position 1)
send(pid, :msg2)
# => Adds :msg2 to mailbox (position 2)
send(pid, :msg3)
# => Adds :msg3 to mailbox (position 3)
# => Mailbox: [:msg1, :msg2, :msg3]

Process.info(pid, :message_queue_len)
# => {:message_queue_len, 3}
# => Returns message count in mailbox

# FIFO processing
receive do
  msg -> IO.inspect(msg, label: "Received")
  # => Processes first message: :msg1
  # => Output: "Received: :msg1"
end
# => Mailbox now: [:msg2, :msg3]

Process.info(pid, :message_queue_len)
# => {:message_queue_len, 2}

# Selective receive (pattern matching)
send(self(), {:priority, "urgent"})
# => Mailbox: [:msg2, :msg3, {:priority, "urgent"}]
send(self(), {:normal, "task1"})
# => Mailbox: [:msg2, :msg3, {:priority, "urgent"}, {:normal, "task1"}]

receive do
  {:priority, content} -> IO.puts("Priority: #{content}")
  # => Scans mailbox for first match
  # => Finds {:priority, "urgent"} at position 3
  # => Skips :msg2 and :msg3
  # => Output: "Priority: urgent"
end
# => Mailbox: [:msg2, :msg3, {:normal, "task1"}]
# => Skipped messages remain in mailbox

# Flush mailbox (process all messages)
receive do
  msg -> IO.inspect(msg)
  # => Process one message
after
  0 -> :ok
  # => Timeout 0ms: if no messages, exit immediately
end
# => Processes messages until mailbox empty

Mailbox Behavior:

OperationEffectPerformance
send/2Adds to end (FIFO)O(1)
receive (no pattern)Removes firstO(1)
receive (with pattern)Scans until matchO(n)
Unmatched messagesStay in mailboxMemory leak risk

Selective Receive Costs:

  • Best case: Match at front of mailbox (O(1))
  • Worst case: Scan entire mailbox (O(n))
  • Memory risk: Unmatched messages accumulate

Key Takeaway: Process mailboxes queue messages in FIFO order. Use receive to process messages. Selective receive scans the mailbox for pattern matches, potentially skipping messages. Monitor mailbox size with Process.info/2 to prevent memory leaks from unmatched messages.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.

Example 58: Anonymous GenServers and Local Names

GenServers can run anonymously (PID-based) or with local/global names. Anonymous GenServers prevent name conflicts and enable multiple instances.

Code:

defmodule Counter do
  # => Anonymous GenServer: identified by PID only
  # => No name registration (no atom name)
  # => Enables multiple instances without name conflicts
  use GenServer
  # => GenServer behavior

  # Start anonymous GenServer (no name)
  # => Anonymous: no name option passed to GenServer.start_link
  # => Callers must track PID to communicate
  def start_link(initial) do
    # => start_link/1: starts anonymous GenServer
    # => initial: initial counter value
    GenServer.start_link(__MODULE__, initial)
    # => No name: option passed
    # => GenServer not registered with any name
    # => __MODULE__: resolves to Counter
    # => initial: passed to init/1
    # => Returns {:ok, pid}
    # => Caller must store pid to use counter
  end

  def increment(pid) do
    # => increment/1: increments counter
    # => pid: GenServer PID (must be passed explicitly)
    GenServer.call(pid, :increment)
    # => GenServer.call/2: synchronous request
    # => Must pass PID explicitly
    # => First arg: pid (not atom name)
    # => Second arg: :increment message
    # => Returns: new counter value
  end

  def get(pid) do
    # => get/1: gets current counter value
    # => pid: required (anonymous GenServer)
    GenServer.call(pid, :get)
    # => Synchronous call with PID
    # => Returns: current state (counter value)
  end

  @impl true
  def init(initial), do: {:ok, initial}
  # => init/1: initializes counter with initial value
  # => Returns: {:ok, state} where state = initial

  @impl true
  def handle_call(:increment, _from, state) do
    # => handle_call/3: handles :increment message
    # => :increment: message from GenServer.call
    # => _from: caller info (ignored)
    # => state: current counter value
    {:reply, state + 1, state + 1}
    # => Increments counter
    # => Reply: state + 1 (new value sent to caller)
    # => New state: state + 1 (updated counter)
    # => Type: {:reply, reply, new_state}
  end

  @impl true
  def handle_call(:get, _from, state) do
    # => handle_call/3: handles :get message
    # => Returns current state without modification
    {:reply, state, state}
    # => Reply: state (current value)
    # => New state: state (unchanged)
  end
end
# => Counter module complete

# Multiple anonymous instances
# => Advantage: multiple counters with different states
# => Each has unique PID (no name conflicts)
{:ok, counter1} = Counter.start_link(0)
# => Start first counter with initial value 0
# => counter1: PID of first GenServer
# => Type: {:ok, #PID<0.150.0>}
{:ok, counter2} = Counter.start_link(100)
# => Start second counter with initial value 100
# => counter2: PID of second GenServer
# => Both counters independent (different PIDs, states)

Counter.increment(counter1)
# => Increment counter1 (0 → 1)
# => Returns: 1
# => 1
Counter.increment(counter2)
# => Increment counter2 (100 → 101)
# => Returns: 101
# => 101
# => Two separate states maintained

Counter.get(counter1)
# => Get counter1 value
# => Returns: 1
# => 1
Counter.get(counter2)
# => Get counter2 value
# => Returns: 101
# => 101
# => Pattern: PID-based identification for multiple instances

# Named GenServer (local to node)
# => Named: registered with atom name
# => Limitation: atoms are limited (not good for dynamic names)
# => Local: only on current node (not distributed)
defmodule NamedCounter do
  # => NamedCounter: GenServer with atom-based naming
  use GenServer

  def start_link(name, initial) do
    # => start_link/2: starts named GenServer
    # => name: atom to register GenServer
    # => initial: initial counter value
    GenServer.start_link(__MODULE__, initial, name: name)
    # => name: name option registers GenServer with atom
    # => name: must be atom (not string or tuple)
    # => Registered globally on local node
    # => Process.whereis(name) → returns PID
    # => Second start_link with same name → {:error, {:already_started, pid}}
  end

  def increment(name), do: GenServer.call(name, :increment)
  # => increment/1: increments named counter
  # => name: atom (not PID)
  # => GenServer.call resolves atom to PID internally
  def get(name), do: GenServer.call(name, :get)
  # => get/1: gets value of named counter
  # => name: atom registered with GenServer

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

  @impl true
  def handle_call(:increment, _from, state), do: {:reply, state + 1, state + 1}
  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}
end
# => NamedCounter module complete

{:ok, _} = NamedCounter.start_link(:counter_a, 0)
# => Start named counter with atom :counter_a
# => Initial value: 0
# => Registered as :counter_a on local node
# => PID discarded (can use name instead)
{:ok, _} = NamedCounter.start_link(:counter_b, 50)
# => Start second named counter with atom :counter_b
# => Initial value: 50
# => Both registered with different atoms

NamedCounter.increment(:counter_a)
# => Increment :counter_a by name (no PID needed)
# => Returns: 1
# => 1
# => Atom resolved to PID internally
NamedCounter.get(:counter_b)
# => Get :counter_b value by name
# => Returns: 50
# => 50
# => Pattern: atom-based lookup (easier than PID tracking)

# Using Registry for dynamic names
# => Registry: OTP process registry (built-in)
# => Enables unlimited dynamic process names
# => Alternative to atoms (atoms are limited resource)
{:ok, _} = Registry.start_link(keys: :unique, name: MyRegistry)
# => Start Registry process
# => keys: :unique - each key registered once (no duplicates)
# => name: MyRegistry - global name for registry
# => :duplicate option allows multiple processes per key
# => Registry must be started before use (usually in supervision tree)

defmodule RegistryCounter do
  # => RegistryCounter: GenServer with Registry-based naming
  # => Enables dynamic names (strings, tuples, not just atoms)
  use GenServer

  def start_link(id, initial) do
    # => start_link/2: starts counter with Registry name
    # => id: dynamic identifier (can be string, tuple, anything)
    GenServer.start_link(__MODULE__, initial, name: via_tuple(id))
    # => name: via_tuple(id) - Registry-based name
    # => via-tuple enables Registry lookup
  end

  defp via_tuple(id) do
    # => via_tuple/1: creates Registry lookup tuple
    # => id: unique identifier for process
    {:via, Registry, {MyRegistry, {:counter, id}}}
    # => {:via, Registry, {registry_name, key}}
    # => :via - tells GenServer to use Registry
    # => Registry - module handling lookup
    # => MyRegistry - registry instance name
    # => {:counter, id} - key in registry (tuple with id)
    # => GenServer uses this to register/lookup process
  end

  def increment(id), do: GenServer.call(via_tuple(id), :increment)
  # => increment/1: increments counter by id
  # => via_tuple(id): resolves to PID via Registry
  # => GenServer.call looks up process in Registry
  def get(id), do: GenServer.call(via_tuple(id), :get)
  # => get/1: gets counter value by id
  # => Registry lookup: {:counter, id} → PID

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

  @impl true
  def handle_call(:increment, _from, state), do: {:reply, state + 1, state + 1}
  @impl true
  def handle_call(:get, _from, state), do: {:reply, state, state}
end
# => RegistryCounter module complete

RegistryCounter.start_link("user_123", 0)
# => Start counter with string id "user_123"
# => Registered as {:counter, "user_123"} in MyRegistry
# => Initial value: 0
# => Dynamic name (not limited by atom table)
RegistryCounter.start_link("user_456", 10)
# => Start counter with string id "user_456"
# => Registered as {:counter, "user_456"} in MyRegistry
# => Initial value: 10
# => Can have thousands/millions of dynamic names

RegistryCounter.increment("user_123")
# => Increment counter for "user_123"
# => Registry lookup: {:counter, "user_123"} → PID
# => Returns: 1
# => 1
RegistryCounter.get("user_456")
# => Get counter value for "user_456"
# => Returns: 10
# => 10
# => Pattern: unlimited dynamic process names via Registry

# Comparison
# 1. Anonymous (PID): Multiple instances, caller tracks PIDs
# => Use: temporary processes, testing, no global access needed
# 2. Named (atoms): Global access, limited names (~1M atoms)
# => Use: singleton services, well-known processes
# 3. Registry (via-tuple): Unlimited dynamic names, global access
# => Use: user sessions, dynamic workers, scalable services

Key Takeaway: Anonymous GenServers use PIDs for identification, enabling multiple instances. Named GenServers use atoms (limited) or Registry (unlimited dynamic names). Use Registry via-tuples for scalable process registration.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 59: Telemetry Events and Metrics

Telemetry provides instrumentation for measuring application behavior. Emit events for metrics, logging, and observability without coupling code to specific reporters.

Code:

# Attach telemetry handler
# => Telemetry: event-based instrumentation library
# => Decouples code instrumentation from metrics/logging
:telemetry.attach(
  # => :telemetry.attach/4: attaches handler to event
  # => First arg: handler id (unique identifier)
  "my-handler-id",
  # => Unique handler ID (string)
  # => Used to detach handler later
  # => Must be unique across all handlers
  [:my_app, :request, :stop],
  # => Event name (list of atoms)
  # => Convention: [app, module, action, lifecycle]
  # => [:my_app, :request, :stop] fired when request completes
  # => Pattern: namespace with atoms to avoid conflicts
  fn event_name, measurements, metadata, _config ->
    # => Callback function: executed when event fires
    # => event_name: the event that triggered ([:my_app, :request, :stop])
    # => measurements: numeric values (duration, count, size)
    # => metadata: contextual data (request path, user id)
    # => _config: config passed to attach (nil here)
    # => Callback receives event data
    IO.puts("Event: #{inspect(event_name)}")
    # => Print event name
    IO.puts("Duration: #{measurements.duration}ms")
    # => Access measurements map (:duration key)
    IO.puts("Metadata: #{inspect(metadata)}")
    # => Print metadata map
  end,
  # => Callback function complete
  nil
  # => Config (passed to callback)
  # => Optional configuration for handler
  # => Available as fourth argument in callback
)
# => Handler attached: ready to receive events

# Emit telemetry event
defmodule MyApp.API do
  # => API module emitting telemetry events
  def handle_request(path) do
    # => handle_request/1: processes request with telemetry
    start_time = System.monotonic_time()
    # => System.monotonic_time/0: monotonic timestamp (nanoseconds)
    # => Monotonic: always increasing (not affected by clock adjustments)
    # => Used for duration measurement (not wall clock)

    # Perform work
    result = process_request(path)
    # => Execute actual request processing
    # => process_request/1: simulated work

    duration = System.monotonic_time() - start_time
    # => Calculate duration in nanoseconds
    # => current_time - start_time = elapsed nanoseconds

    # Emit telemetry event
    :telemetry.execute(
      # => :telemetry.execute/3: emits event to all attached handlers
      [:my_app, :request, :stop],
      # => Event name (must match handler event name)
      # => All handlers attached to this event will fire
      %{duration: duration},
      # => Measurements (numeric data)
      # => Map with measurement values
      # => Convention: numeric metrics (duration, count, bytes)
      %{path: path, result: result}
      # => Metadata (any term)
      # => Contextual information about event
      # => Can be any data (request details, user info, etc.)
    )
    # => execute/3 complete: handlers fired synchronously

    result
    # => Return result to caller
  end

  defp process_request(path) do
    # => Simulated request processing
    :timer.sleep(100)
    # => Sleep 100ms (simulate work)
    {:ok, "Response for #{path}"}
    # => Return simulated response
  end
end
# => MyApp.API complete

MyApp.API.handle_request("/users")
# => Call handle_request with "/users" path
# => Emits [:my_app, :request, :stop] event
# => Handler callback fires and prints:
# Prints:
# Event: [:my_app, :request, :stop]
# Duration: 100000000ms
# => Duration in nanoseconds (100ms = 100,000,000ns)
# Metadata: %{path: "/users", result: {:ok, "Response for /users"}}

# Span measurement pattern
# => Span: measures duration automatically (start → stop events)
# => Simplifies manual start_time/duration tracking
defmodule MyApp.Database do
  # => Database module with span telemetry
  def query(sql) do
    # => query/1: executes query with automatic timing
    :telemetry.span(
      # => :telemetry.span/3: emits start and stop events automatically
      # => Fires: [:my_app, :db, :query, :start] before function
      # => Fires: [:my_app, :db, :query, :stop] after function
      [:my_app, :db, :query],
      # => Event prefix (adds :start and :stop suffixes)
      # => [:my_app, :db, :query] → [:my_app, :db, :query, :start/stop]
      %{sql: sql},
      # => Initial metadata (available in both start and stop events)
      # => Metadata for query SQL
      fn ->
        # => Function to execute (work to measure)
        # => span measures duration of this function
        # Perform work
        result = execute_query(sql)
        # => Execute actual query

        # Return {result, extra_metadata}
        {result, %{rows: length(result)}}
        # => Return tuple: {result, extra_metadata}
        # => result: returned to caller
        # => extra_metadata: merged into stop event metadata
        # => Pattern: add runtime-computed metadata (:rows count)
      end
    )
    # => span/3 returns result from function
    # => Emits :start event before function
    # => Emits :stop event after function (with duration measurement)
  end

  defp execute_query(_sql) do
    # => Simulated query execution
    :timer.sleep(50)
    # => Sleep 50ms (simulate database query)
    [{:id, 1, :name, "Alice"}, {:id, 2, :name, "Bob"}]
    # => Return mock query results (list of tuples)
  end
end
# => MyApp.Database complete

# Attach handler for database queries
:telemetry.attach(
  # => Attach handler for database span stop event
  "db-handler",
  # => Handler ID
  [:my_app, :db, :query, :stop],
  # => Event name: span stop event
  # => Note: :stop suffix added by span/3
  fn _event, measurements, metadata, _config ->
    # => Handler callback
    # => _event: [:my_app, :db, :query, :stop]
    # => measurements: %{duration: nanoseconds}
    # => metadata: %{sql: "...", rows: count}
    IO.puts("Query took #{measurements.duration}ns")
    # => Print query duration (nanoseconds)
    # => measurements.duration: auto-computed by span/3
    IO.puts("Returned #{metadata.rows} rows")
    # => Print row count
    # => metadata.rows: from extra_metadata tuple
  end,
  nil
  # => No config
)
# => Handler attached for database queries

MyApp.Database.query("SELECT * FROM users")
# => Execute query with telemetry
# => Emits: [:my_app, :db, :query, :start]
# => Executes query (50ms)
# => Emits: [:my_app, :db, :query, :stop] with duration
# => Handler prints query timing and row count

# Multiple handlers for same event
# => attach_many/4: attaches handler to multiple events
# => Alternative to multiple attach/4 calls
:telemetry.attach_many(
  # => :telemetry.attach_many/4: one handler for multiple events
  "multi-handler",
  # => Handler ID (unique)
  [
    # => List of events to handle
    [:my_app, :request, :start],
    # => Request start event
    [:my_app, :request, :stop],
    # => Request stop event
    [:my_app, :request, :exception]
    # => Request exception event
  ],
  # => Same handler fires for all three events
  fn event, measurements, metadata, _config ->
    # => Handler callback for any of the three events
    # => event: which event fired (varies)
    Logger.info("Event: #{inspect(event)}", measurements: measurements, metadata: metadata)
    # => Log event with measurements and metadata
    # => Logger.info/2: logs at info level
    # => Keyword list: [measurements: ..., metadata: ...]
  end,
  nil
  # => No config
)
# => Handler attached to 3 events
# => Pattern: single handler for related events (request lifecycle)

Key Takeaway: Telemetry decouples instrumentation from reporting. Emit events with :telemetry.execute/3 for measurements and :telemetry.span/3 for start/stop events. Attach handlers to process events for metrics, logging, or monitoring.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


Example 60: Type Specifications with @spec

Type specs document function signatures and enable static analysis with Dialyzer. They improve code documentation and catch type errors at compile time.

@spec Syntax

@spec declares type specifications for functions:

  • @spec function_name(param_types) :: return_type
  • Place before function definition
  • :: separates parameters from return type
  • Dialyzer verifies implementation matches spec

Common Built-in Types

TypeDescriptionExample Values
integer()Whole numbers-2, -1, 0, 1, 2, ...
float()Decimal numbers3.14, 2.5, -1.0
number()Integer or floatAny numeric value
String.t()UTF-8 binary string"hello", "world"
atom()Atom literals:ok, :error, :atom
boolean()True/falsetrue, false
list(type)List of specific type[1, 2, 3] for list(integer())
map()Any map%{key: value}
tuple()Any tuple{:ok, "value"}
pid()Process identifierResult of spawn/1
term()Any Elixir termWildcard type
keyword()Keyword list[key: value, ...]
any()Any typeWildcard, no constraints

Union Types

Use | to specify multiple possible types:

  • {:ok, float()} | {:error, atom()} - Either success or error tuple
  • integer() | nil - Integer or nil (optional value)
  • Common pattern: Result tuples with union types

Code:

defmodule Calculator do
  @spec add(integer(), integer()) :: integer()
  # => Spec declares: two integers → integer
  # => Dialyzer validates implementation matches this signature
  def add(a, b), do: a + b
  # => Returns: sum (integer)
  # => Example: add(2, 3) => 5

  @spec divide(number(), number()) :: {:ok, float()} | {:error, atom()}
  # => Spec: union type (success OR error)
  # => Union type: {:ok, float()} | {:error, atom()} means return one of two tuples
  def divide(_a, 0), do: {:error, :division_by_zero}
  # => Returns: error tuple when divisor is zero
  # => Pattern match: 0 as divisor triggers this clause
  def divide(a, b), do: {:ok, a / b}
  # => Returns: success tuple with float
  # => Normal case: wraps division result in :ok tuple

  @spec sum(list(number())) :: number()
  # => Spec: list of numbers → number
  # => Accepts list of integers, floats, or mixed
  def sum(numbers), do: Enum.sum(numbers)
  # => Enum.sum: returns sum (integer or float)
  # => Example: sum([1, 2, 3]) => 6

  @spec abs(integer()) :: integer()
  # => Single spec covers all function clauses
  # => All clauses must match same return type
  def abs(n) when n < 0, do: -n
  # => Clause 1: negative → positive
  # => Guard: when n < 0 only matches negative numbers
  def abs(n), do: n
  # => Clause 2: positive → unchanged
  # => Fallthrough: matches positive and zero
end

defmodule User do
  @type t :: %__MODULE__{
    id: integer(),
    name: String.t(),
    email: String.t(),
    age: integer() | nil
  }
  # => Custom type: User.t with field types
  # => @type t :: ... defines reusable type for this struct

  defstruct [:id, :name, :email, :age]
  # => Defines struct with four fields

  @spec new(integer(), String.t(), String.t()) :: t()
  # => Returns: User.t custom type
  # => Constructor function: creates new User struct
  def new(id, name, email) do
    %__MODULE__{id: id, name: name, email: email}
    # => age defaults to nil (not provided)
    # => %__MODULE__{} expands to %User{} at compile time
  end

  @spec update_age(t(), integer()) :: t()
  # => Spec: User.t, integer → User.t
  # => Takes user struct and new age, returns updated struct
  def update_age(user, age) do
    %{user | age: age}
    # => Returns: updated User struct
    # => Map update syntax: %{struct | field: new_value}
  end

  @spec display(t()) :: String.t()
  # => Spec: User.t → String
  # => Pattern matches on struct fields in parameters
  def display(%__MODULE__{name: name, email: email}) do
    "#{name} (#{email})"
    # => Returns: formatted string
    # => String interpolation: "Alice (alice@example.com)"
  end
end

defmodule StringHelper do
  @spec reverse(String.t()) :: String.t()
  # => Spec: string → string
  def reverse(string), do: String.reverse(string)
  # => "hello" → "olleh"
  # => String.reverse/1 reverses character order

  @spec split(String.t(), String.t()) :: list(String.t())
  # => Spec: string, separator → list of strings
  # => Returns list of string parts
  def split(string, separator), do: String.split(string, separator)
  # => "a,b,c" split by "," → ["a", "b", "c"]
  # => String.split/2 breaks string at separator

  @spec join(list(String.t()), String.t()) :: String.t()
  # => Spec: list of strings, separator → string
  # => Inverse of split operation
  def join(parts, separator), do: Enum.join(parts, separator)
  # => ["a", "b", "c"] with "," → "a,b,c"
  # => Enum.join/2 concatenates list elements with separator
end

# Custom type aliases
@type result :: {:ok, String.t()} | {:error, atom()}
# => Reusable type: success or error tuple
# => Defines common result pattern for this module
@type user_id :: integer()
# => Semantic alias: clearer intent than raw integer()
# => Type alias documents domain meaning
@type user_map :: %{id: user_id(), name: String.t()}
# => Map type with required keys
# => Specifies exact map structure expected

@spec find_user(user_id()) :: result()
# => Uses custom types for readability
# => Returns: {:ok, String.t()} | {:error, atom()}
def find_user(id) when id > 0, do: {:ok, "User #{id}"}
# => Valid id: returns success tuple
# => Guard: id > 0 ensures positive ID
def find_user(_id), do: {:error, :invalid_id}
# => Invalid id: returns error tuple
# => Fallthrough clause handles invalid IDs

# mix dialyzer
# => Runs static type analysis
# => First run: builds PLT (Persistent Lookup Table - slow)
# => Subsequent: fast incremental checks
# => Reports: type mismatches, spec violations
# => Example output: "Function returns {error, binary()} but spec says {error, atom()}"

Key Takeaway: Use @spec to document function types. Define custom types with @type. Type specs enable Dialyzer to catch type errors and improve documentation. Common types: integer(), String.t(), list(type), map(), {:ok, type} | {:error, reason}.

Why It Matters: This concept is fundamental to understanding the language and helps build robust, maintainable code.


What’s Next?

You’ve completed the intermediate examples covering advanced pattern matching, data structures, module organization, error handling, processes, testing, and OTP fundamentals. You now understand:

  • Advanced pattern matching with guards and with
  • Structs, streams, and MapSets
  • Module attributes, import/alias, and protocols
  • Error handling with result tuples and try/rescue
  • Process spawning, message passing, and monitoring
  • Task abstraction and testing with ExUnit
  • Supervisor child specs and application lifecycle
  • Custom Mix tasks and runtime configuration
  • Process links, mailbox management, and GenServer patterns
  • Telemetry instrumentation and type specifications

Continue your learning:

Deepen your understanding:

Last updated