Elixir Fundamentals: Functional Programming and OTP · Functional Elixir: Data, Pattern Matching and Functions
Control Flow: case, cond, if and with
Choose between case (match a value against patterns), cond (first true condition), if/unless (one boolean), and with (a happy-path pipeline of steps that each must succeed).
What you'll build
A sign-up flow that validates an email, checks the password, and creates an account, written with with so the happy path reads top to bottom and any failure stops early with a clear error.
The idea
case value do pattern -> ... end: match one value against patterns (with optional guards).cond do condition -> ... end: the first condition that's truthy wins (usetrue ->as the fallback).if/unless: for a single boolean. Note thatifis an expression and returns a value (ornil).with: chains steps that return{:ok, value}. Each<-must match; if one doesn't,withstops and returns the non-matching value (or runs theelseclauses). It's Elixir's answer to nestedcasestatements.
Only false and nil are falsy; everything else (including 0 and "") is truthy.
The code
defmodule Signup do
def validate_email(email) do
if String.match?(email, ~r/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
do: {:ok, String.downcase(email)},
else: {:error, :invalid_email}
end
def validate_password(password) do
cond do
String.length(password) < 8 -> {:error, :password_too_short}
not String.match?(password, ~r/\d/) -> {:error, :password_needs_a_digit}
true -> {:ok, password}
end
end
def ensure_unique(email, taken) do
case MapSet.member?(taken, email) do
true -> {:error, :email_taken}
false -> {:ok, email}
end
end
def register(params, taken) do
with {:ok, email} <- validate_email(params.email),
{:ok, email} <- ensure_unique(email, taken),
{:ok, _password} <- validate_password(params.password) do
{:ok, %{email: email, id: :erlang.phash2(email, 10_000)}}
else
{:error, :email_taken} -> {:error, "that email already has an account"}
{:error, reason} -> {:error, reason |> Atom.to_string() |> String.replace("_", " ")}
end
end
end
taken = MapSet.new(["[email protected]"])
for params <- [
%{email: "[email protected]", password: "correct-horse-9"},
%{email: "[email protected]", password: "whatever-123"},
%{email: "not-an-email", password: "x"},
%{email: "[email protected]", password: "short"},
%{email: "[email protected]", password: "no-digits-here"}
] do
case Signup.register(params, taken) do
{:ok, user} -> IO.puts("created #{user.email}")
{:error, message} -> IO.puts("#{params.email}: #{message}")
end
end
IO.inspect(if(0, do: "0 is truthy", else: "0 is falsy"))
Output
created [email protected] [email protected]: that email already has an account not-an-email: invalid email [email protected]: password too short [email protected]: password needs a digit "0 is truthy"
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Nesting
caseinsidecasefor multi-step flows;withflattens them. - Forgetting
true ->incond, which raises when nothing matches. - Assuming
0or""is falsy. Onlyfalseandnilare. - A
withwithoutelsethat leaks raw errors to callers; translate them where it matters.
Key points
casefor patterns,condfor conditions,iffor one boolean.- Only
falseandnilare falsy. withchains{:ok, _}steps and short-circuits on the first failure.- Use
elseinwithto turn internal errors into friendly ones.
Further reading: case, cond, and if, with.
Practice questions
When would you use `with` in Elixir?
Answer
For a sequence of steps that each return {:ok, value} (or similar) where any failure should stop the flow: with chains the matches and returns the first non-matching value, or handles it in an else block, avoiding nested case statements.
Which values are falsy in Elixir?
Answer
Only false and nil. Everything else, including 0, empty strings and empty lists, is truthy.
Open in the app to mark lessons complete, take notes and track your progress.