Phase 1, Session 1: Basics & Collections
Overview
This session covers the fundamental building blocks of Elixir:
- Basic data types (integers, floats, atoms, strings, booleans)
- Collections (lists, tuples, maps, keyword lists)
Goal: Understand Elixir’s data types and why they matter for building an agent framework.
Why These Data Types Matter for Your Agent Framework
Before diving into syntax, understand that Elixir was designed for:
-
Immutability - Data never changes in place. When you “update” something, you get a new copy. This is critical for concurrent agents that shouldn’t step on each other’s data.
-
Pattern Matching - Every data type can be destructured. This makes message handling elegant.
-
Message Passing - Atoms and tuples form the backbone of inter-process communication. You’ll see patterns like
{:ok, result}and{:error, reason}everywhere.
Section 1: Basic Data Types
1.1 Integers
Elixir supports multiple integer formats:
# Decimal (base 10)
decimal = 255
# Binary (base 2) - prefix with 0b
binary = 0b11111111
# Octal (base 8) - prefix with 0o
octal = 0o377
# Hexadecimal (base 16) - prefix with 0x
hex = 0xFF
# All represent the same value
{decimal, binary, octal, hex}
# All of these equal 255
255 == 0b11111111 and 0b11111111 == 0o377 and 0o377 == 0xFF
1.2 Floats
Floats require a decimal point with at least one digit on each side:
# Valid floats
pi = 3.14
tiny = 1.0e-10 # Scientific notation
big = 2.5e3 # 2500.0
{pi, tiny, big}
# IMPORTANT: Division ALWAYS returns a float in Elixir
10 / 2
# For integer division, use div/2 and rem/2
{div(10, 3), rem(10, 3)}
1.3 Atoms
Atoms are constants where the name IS the value. They’re used extensively for:
-
Status codes (
:ok,:error) - Keys in maps
- Module names
- Boolean values
# Basic atoms - the last expression is returned
[:hello, :agent_ready, :processing]
# Atoms with spaces or special characters need quotes
{:"agent-1", :"with spaces"}
# IMPORTANT: Booleans are just atoms!
{true == :true, false == :false, nil == :nil}
For your agent framework: Atoms are perfect for agent states like :idle, :busy, :waiting, :error.
1.4 Strings
Strings are UTF-8 encoded and use double quotes:
# Basic string
"Hello, World!"
# String interpolation with #{}
agent_name = "Researcher"
"Agent #{agent_name} is ready"
# String concatenation with <>
"Hello" <> " " <> "World"
# Multi-line strings
"""
This is a
multi-line
string
"""
1.5 Booleans and Truthiness
# Only two boolean values
{true, false}
# CRITICAL: In Elixir, ONLY false and nil are falsy
# Everything else is truthy, including:
# - 0 (zero)
# - "" (empty string)
# - [] (empty list)
if 0, do: "0 is truthy!", else: "0 is falsy"
if "", do: "empty string is truthy!", else: "empty string is falsy"
Section 2: Operators
Elixir has several categories of operators. Understanding when to use each is important.
2.1 Arithmetic Operators
# Basic arithmetic
addition = 10 + 3 # 13
subtraction = 10 - 3 # 7
multiplication = 10 * 3 # 30
{addition, subtraction, multiplication}
# Division - ALWAYS returns a float!
{10 / 3, 10 / 2} # {3.333..., 5.0}
# Integer division and remainder
{div(10, 3), rem(10, 3)} # {3, 1}
# Negative numbers with rem (follows sign of dividend)
{rem(-10, 3), rem(10, -3)} # {-1, 1}
2.2 Comparison Operators
# Equality (value comparison, allows type coercion)
{1 == 1.0, 1 == 1, "a" == "a"} # {true, true, true}
# Strict equality (type must match too)
{1 === 1.0, 1 === 1} # {false, true}
# Inequality
1 != 2 # true (not equal)
1 !== 1.0 # true (strict not equal)
# Ordering
5 > 3 # true
5 >= 5 # true
3 < 5 # true
3 <= 3 # true
# IMPORTANT: Cross-type comparison has a defined order
# number < atom < reference < function < port < pid < tuple < map < list < bitstring
1 < :atom # true (numbers come before atoms)
:atom < {1, 2} # true (atoms come before tuples)
{1, 2} < [1, 2] # true (tuples come before lists)
2.3 Boolean Operators
Elixir has TWO sets of boolean operators with different behaviors:
Strict boolean operators (and, or, not) - require boolean arguments:
# and, or, not - REQUIRE true/false on left side
true and true # true
true and false # false
true or false # true
not true # false
# These will raise an error if left side isn't boolean:
# 1 and true # ** (BadBooleanError)
# nil or true # ** (BadBooleanError)
Relaxed boolean operators (&&, ||, !) - work with any type (truthy/falsy):
# && returns first falsy value, or last value if all truthy
1 && 2 && 3 # 3 (all truthy, returns last)
1 && nil && 3 # nil (returns first falsy)
1 && false && 3 # false
# || returns first truthy value, or last value if all falsy
nil || false || 3 # 3 (returns first truthy)
1 || 2 || 3 # 1 (returns first truthy)
nil || false # false (all falsy, returns last)
# ! negates truthiness
!true # false
!nil # true
!1 # false (1 is truthy)
!"hello" # false (strings are truthy)
!!nil # false (double negation to get boolean)
When to use which:
-
Use
and/or/notwhen you KNOW you have booleans (safer, clearer intent) -
Use
&&/||/!when working with truthy/falsy values or for short-circuit defaults
# Common pattern: default values with ||
name = nil
display_name = name || "Anonymous" # "Anonymous"
# Common pattern: conditional execution with &&
should_log = true
should_log && IO.puts("Logging enabled") # prints and returns :ok
2.4 String Operators
# Concatenation with <>
"Hello" <> " " <> "World" # "Hello World"
# Building strings
prefix = "Agent"
name = "Researcher"
prefix <> "-" <> name # "Agent-Researcher"
# Interpolation (preferred for readability)
"#{prefix}-#{name}" # "Agent-Researcher"
2.5 List Operators
# Concatenation with ++
[1, 2] ++ [3, 4] # [1, 2, 3, 4]
# Subtraction with -- (removes first occurrence of each)
[1, 2, 3, 2, 1] -- [1, 2] # [3, 2, 1]
# Cons operator | (prepend to head - FAST!)
[0 | [1, 2, 3]] # [0, 1, 2, 3]
# Multiple prepends
[-1, 0 | [1, 2, 3]] # [-1, 0, 1, 2, 3]
2.6 Match Operator (=)
This is NOT assignment - it’s pattern matching! (Covered in depth in Session 2)
# The = operator tries to MATCH left side to right side
x = 1 # matches x to 1
1 = x # works! x is 1, so 1 = 1 matches
# 2 = x # would fail! 2 doesn't match 1
# Destructuring with =
{a, b} = {1, 2} # a = 1, b = 2
[head | tail] = [1,2,3] # head = 1, tail = [2, 3]
%{name: n} = %{name: "Alice", age: 30} # n = "Alice"
2.7 Pipe Operator (|>)
Passes the result of the left expression as the first argument to the right function. (Covered in depth in Session 3)
# Without pipe - nested, hard to read
String.upcase(String.trim(" hello "))
# With pipe - left to right, clear data flow
" hello "
|> String.trim()
|> String.upcase()
Operators Summary
| Category | Operators | Notes |
|---|---|---|
| Arithmetic |
+, -, *, /, div, rem |
/ always returns float |
| Comparison |
==, !=, ===, !==, <, >, <=, >= |
=== is strict |
| Boolean (strict) |
and, or, not |
Require boolean left side |
| Boolean (relaxed) | &&, ||, ! | Work with truthy/falsy |
| String | <> | Concatenation |
| List | ++, --, | | | for prepend |
| Match | = | Pattern matching |
| Pipe | |> | Chain functions |
Section 3: Collections
3.1 Lists
Lists are ordered, linked lists. They can contain any type and allow duplicates.
# Creating lists
[1, 2, 3]
["hello", :world, 42, 3.14]
Key insight: Lists are linked lists, so:
- Prepending is O(1) - fast!
- Appending is O(n) - slow!
- Getting length is O(n)
# Prepending (FAST) - use the | operator
list = [2, 3, 4]
[1 | list]
# Appending (SLOW) - avoid in loops
list = [1, 2, 3]
list ++ [4]
# List concatenation
[1, 2] ++ [3, 4]
# List subtraction (removes first occurrence)
[1, 2, 3, 2, 1] -- [1, 2]
# Head and Tail - fundamental operations
list = [1, 2, 3, 4]
{hd(list), tl(list)}
# Pattern matching with head | tail (you'll use this constantly!)
[head | tail] = [1, 2, 3, 4]
{head, tail}
For your agent framework: Lists are perfect for message queues/inboxes where you prepend new messages and process from the head.
3.2 Tuples
Tuples are fixed-size, contiguous in memory. Use them for structured data with a known number of elements.
# Creating tuples
{:ok, "success"}
{:error, :not_found}
{:agent, "researcher", :idle}
# Accessing elements (0-indexed)
tuple = {:ok, "data", 42}
elem(tuple, 0) # :ok
elem(tuple, 1) # "data"
# Tuple size is O(1) - fast!
tuple_size({:a, :b, :c, :d})
Common patterns you’ll see everywhere:
# Success/failure pattern
{:ok, result} = {:ok, "the data"}
result
# Error pattern
{:error, reason} = {:error, :timeout}
reason
For your agent framework: Tuples are the standard format for messages between processes: {:task, task_id, payload}, {:response, task_id, result}.
3.3 Maps
Maps are key-value stores. Keys can be any type. This is your go-to for structured data.
# Creating maps
%{"name" => "Alice", "age" => 30}
# With atom keys (most common)
%{name: "Alice", age: 30, status: :active}
# Mixed keys
%{:name => "Alice", "role" => "researcher", 1 => "first"}
Accessing values:
agent = %{name: "Researcher", state: :idle, tasks_completed: 0}
# Bracket notation (works with any key type, returns nil if missing)
agent[:name]
# Dot notation (ONLY for atom keys, raises if missing)
agent.name
# Map.get with default value
Map.get(agent, :missing_key, "default")
Updating maps:
agent = %{name: "Researcher", state: :idle}
# Update syntax (key must exist!)
%{agent | state: :busy}
# Map.put (works for new or existing keys)
agent
|> Map.put(:state, :busy)
|> Map.put(:current_task, "analyze_data")
For your agent framework: Maps are perfect for agent state: %{name: "researcher", state: :idle, inbox: [], memory: %{}}.
3.4 Keyword Lists
Keyword lists are lists of 2-tuples with atom keys. They preserve order and allow duplicate keys.
# These are equivalent:
[name: "Alice", role: "admin"]
[{:name, "Alice"}, {:role, "admin"}]
# Duplicate keys allowed
[a: 1, a: 2, b: 3]
# Access (returns first match)
opts = [timeout: 5000, retry: true, timeout: 3000]
opts[:timeout] # 5000 (first one)
Primary use: Function options
# This is why you see function calls like:
# SomeModule.function(arg, timeout: 5000, async: true)
# The keyword list is the last argument
defmodule Example do
def greet(name, opts \\ []) do
title = Keyword.get(opts, :title, "")
"Hello, #{title} #{name}!"
end
end
Example.greet("Alice", title: "Dr.")
Section 4: Hands-On Exercises
Exercise 1: Create an Agent State Map
Create a map representing an agent with:
- name (string)
- state (atom: :idle, :busy, or :waiting)
- inbox (list of messages)
- memory (map for storing context)
# Your solution:
agent = %{
name: "Researcher",
state: :idle,
inbox: [],
memory: %{}
}
Exercise 2: Add Messages to Inbox
Given the agent above, add some messages to its inbox. Remember: prepending is fast!
# Messages as tuples
msg1 = {:task, "task-001", %{action: "summarize", content: "..."}}
msg2 = {:task, "task-002", %{action: "analyze", content: "..."}}
# Add messages (prepend for efficiency)
agent = %{agent | inbox: [msg1 | agent.inbox]}
agent = %{agent | inbox: [msg2 | agent.inbox]}
agent.inbox
Exercise 3: Process a Message
Extract the first message from the inbox and update the agent state:
# Pattern match to get head and tail
[current_message | remaining] = agent.inbox
# Update agent
agent = %{agent |
inbox: remaining,
state: :busy
}
{current_message, agent}
Exercise 4: Using Tuples for Message Types
Create different message types and pattern match on them:
# Different message types
task_msg = {:task, "id-1", %{action: "search"}}
response_msg = {:response, "id-1", {:ok, "results"}}
error_msg = {:error, "id-1", :timeout}
# Pattern match to handle each type
handle_message = fn
{:task, id, payload} -> "Processing task #{id}"
{:response, id, result} -> "Got response for #{id}"
{:error, id, reason} -> "Error on #{id}: #{reason}"
end
handle_message.(task_msg)
Summary
| Type | Syntax | Use Case |
|---|---|---|
| Integer |
42, 0xFF |
Counts, IDs |
| Float |
3.14, 1.0e-10 |
Calculations |
| Atom |
:ok, :error |
Status, keys, states |
| String |
"hello" |
Text |
| Boolean |
true, false |
Conditions |
| List |
[1, 2, 3] |
Ordered collections, queues |
| Tuple |
{:ok, value} |
Fixed structures, messages |
| Map |
%{key: value} |
State, structured data |
| Keyword List |
[key: val] |
Function options |
Key Takeaways for Agent Framework
- Agent state → Map with atom keys
- Message inbox → List (prepend new, process from head)
-
Messages → Tuples like
{:task, id, payload} -
Agent status → Atoms like
:idle,:busy,:error - Function options → Keyword lists
Next Session
Session 2: Pattern Matching - The most important concept in Elixir/Erlang!