Day 04 Advanced Topics

Functional Patterns & Metaprogramming

Elixir inherits Erlang's functional patterns and adds powerful metaprogramming via macros. Today covers Enum and Stream for data processing, protocols for polymorphism, and macros that extend the language itself.

~1 hour Hands-on Precision AI Academy

Today’s Objective

Elixir inherits Erlang's functional patterns and adds powerful metaprogramming via macros. Today covers Enum and Stream for data processing, protocols for polymorphism, and macros that extend the language itself.

Enum processes collections eagerly — all at once. Stream processes lazily — one element at a time, on demand. Enum.map/filter/reduce/sort/group_by cover most data processing needs. For large collections or infinite streams, Stream.map/filter/take work the same way but only pull elements when needed. 'Stream.cycle([1,2,3]) |> Enum.take(10)' produces [1,2,3,1,2,3,1,2,3,1] without ever creating an infinite list in memory.

Protocols: Polymorphism

A Protocol defines an interface that different data types implement. Protocol.derive/2 can auto-derive implementations. Built-in protocols: Enumerable (makes a type work with Enum), Collectable (can receive elements), Inspect (custom iex display), String.Chars (to_string conversion). Define: 'defprotocol Serializable do; def serialize(term); end'. Implement: 'defimpl Serializable, for: MyStruct do...'.

Macros

Elixir macros run at compile time and generate AST nodes. defmacro defines a macro. quote/2 captures code as AST. unquote/1 injects values into quoted expressions. Macros are why Elixir can implement features like defmodule, def, if, and use as library code, not built-in syntax. Rule: use functions when possible; reach for macros only when you need to transform syntax or generate repetitive code.

elixir
ELIXIR
# Enum vs Stream comparison
# Enum: eager, processes all at once
1..1_000_000
|> Enum.map(&(&1 * 2))
|> Enum.filter(&(rem(&1, 3) == 0))
|> Enum.take(5)
# Creates 1M intermediate list

# Stream: lazy, only computes what's needed
1..1_000_000
|> Stream.map(&(&1 * 2))
|> Stream.filter(&(rem(&1, 3) == 0))
|> Enum.take(5)  # Triggers evaluation
# [6, 12, 18, 24, 30] -- O(n) not O(n^2)

# Protocol example
defprotocol Area do def calculate(shape)
end

defimpl Area, for: %{type: :circle} do def calculate(%{r: r}), do: :math.pi() * r * r
end

# Macro example: unless (inverse of if)
defmacro unless(condition, do: block) do quote do if !unquote(condition), do: unquote(block) end
end

unless 1 == 2 do IO.puts "1 is not 2"
end
Prefer Stream over Enum when: (1) the collection is large (100K+ elements), (2) you only need part of the result (take/N), or (3) you are reading from a file or network. For small collections, Enum is simpler and fast enough.
📝 Day 4 Exercise Process a Large Dataset with Stream
  1. Generate a CSV file with 500,000 rows using a script
  2. Read it lazily with File.stream! and pipe through Stream.map/filter
  3. Compute the sum and average of a numeric column without loading all rows into memory
  4. Compare memory usage with Enum.to_list (eager) vs Stream (lazy) using :erlang.memory/0
  5. Define a protocol Describable with a describe/1 function and implement it for 3 custom structs

Supporting Resources

Go deeper with these references.

Elixir Docs
Official Elixir Documentation Complete language reference including guides, library docs, and mix tasks.
HexDocs
Phoenix Framework Docs Full reference for Phoenix, the most popular Elixir web framework.
Elixir School
Elixir School Curriculum Community-driven step-by-step curriculum for learning Elixir.

Day 4 Checkpoint

Before moving on, make sure you can answer these without looking:

Continue To Day 5
Distributed Elixir & Production