Add with statement error demos and polish LiveBook presentation

Torey Heinz committed Feb 22, 2026
commit 3e7d8e8fb5af60688a48eb00927a6f3dd7f46ace
Showing 1 changed file with 60 additions and 80 deletions
02-the-language.livemd +60 -80
@@ @@ -2,11 +2,11 @@
## What Makes Elixir Click
- Not a full tutorial — just the "aha" moments that make Elixir different.
+ Just a couple of "aha" moments that make Elixir different.
We'll cover three things:
- 1. **Pattern Matching** — the feature that changes how you think about code
+ 1. **Pattern Matching** — a feature that changes how you think about code
2. **Processes & Concurrency** — lightweight, isolated, fault-tolerant
3. **LiveView** — real-time web UIs without JavaScript
@@ @@ -41,28 +41,29 @@ IO.puts("#{name} from #{city}")
```elixir
# Match on a specific value — this is where it gets interesting
{:ok, result} = {:ok, 42}
+
IO.puts("Got: #{result}")
```
```elixir
# What happens when the match FAILS?
- # Uncomment the line below and run it:
- # {:ok, result} = {:error, "something went wrong"}
+ {:ok, result} = {:error, "something went wrong"}
```
That's not a bug — it's a feature. If you expect `:ok` and get `:error`,
Elixir tells you immediately instead of silently continuing with bad data.
- **For the JS folks:** Think of it like destructuring on steroids — but it also
+ **JavaScript:** Simalar to destructuring — but it also
validates the shape of your data at the same time.
- **For the Rails folks:** No more `if result.success?` checks scattered everywhere.
-
## Pattern Matching: Function Heads
- This is where it really clicks. In Elixir, you can define the **same function
- multiple times** with different patterns. Elixir picks the first one that matches.
+ In Elixir, you can define the **same function multiple times** with different patterns. Elixir picks the first one that matches.
+
+ At first I thought this was a bad idea, calling/creating a different function, based on the args... but then it clicked!
+
+ Instead of creating if/case statements to handle different inputs with a single function, you can create focused functions based on the inputs.
```elixir
defmodule Greeter do
@@ @@ -160,8 +161,7 @@ what case it handles.
## Pattern Matching: The `with` Statement
- Real-world code often has multiple steps that can each fail. Here's a pattern
- we use heavily in production — inspired by our email campaign system:
+ Real-world code often has multiple steps that can each fail. In other langauges there's an Interactor pattern that often requires a whole library to implement. Elixir has the built in `with` Statement
```elixir
defmodule OrderProcessor do
@@ @@ -181,12 +181,15 @@ defmodule OrderProcessor do
# Simulated steps — in production these hit databases and APIs
defp find_order(1), do: {:ok, %{id: 1, item: "Elixir Book", amount: 29_99}}
+ defp find_order(2), do: {:ok, %{id: 2, item: "Sold Out Tee", amount: 19_99}}
+ defp find_order(3), do: {:ok, %{id: 3, item: "Elixir Book", amount: 99_99}}
defp find_order(_), do: {:error, :not_found}
defp validate_inventory(%{item: "Elixir Book"}), do: {:ok, :in_stock}
defp validate_inventory(_), do: {:error, :out_of_stock}
- defp charge_payment(%{amount: amount}), do: {:ok, %{charge_id: "ch_123", amount: amount}}
+ defp charge_payment(%{amount: amount}) when amount < 50_00, do: {:ok, %{charge_id: "ch_123", amount: amount}}
+ defp charge_payment(_), do: {:error, :payment_declined}
defp create_shipment(order), do: {:ok, %{tracking: "1Z999AA10123456784", order_id: order.id}}
end
@@ @@ -202,6 +205,16 @@ OrderProcessor.process_order(1)
OrderProcessor.process_order(999)
```
+ ```elixir
+ # Out of stock — fails at validate_inventory step
+ OrderProcessor.process_order(2)
+ ```
+
+ ```elixir
+ # Payment declined — fails at charge_payment step
+ OrderProcessor.process_order(3)
+ ```
+
The `with` statement chains operations that return `{:ok, value}` or `{:error, reason}`.
If any step fails, it jumps straight to the `else` block.
@@ @@ -237,42 +250,6 @@ IO.puts("Hello from the main process #{inspect(self())}")
IO.puts("Spawned 10,000 processes in #{time_microseconds / 1_000}ms")
```
- ## Processes: Message Passing
-
- Processes communicate by **sending messages**. No shared state, no locks, no race conditions.
-
- ```elixir
- # The current process sends a message to itself
- send(self(), {:greeting, "Hello from GR Web Dev!"})
-
- # Receive pattern matches on messages in the mailbox
- receive do
- {:greeting, message} -> IO.puts("Got: #{message}")
- other -> IO.puts("Unexpected: #{inspect(other)}")
- after
- 1000 -> IO.puts("No message received within 1 second")
- end
- ```
-
- ```elixir
- # A more realistic example: spawn a worker and get a result back
- parent = self()
-
- spawn(fn ->
- # Simulate some work
- Process.sleep(100)
- result = Enum.sum(1..1_000_000)
- # Send the result back to the parent
- send(parent, {:result, result})
- end)
-
- receive do
- {:result, sum} -> IO.puts("Sum of 1 to 1,000,000 = #{sum}")
- after
- 5000 -> IO.puts("Timed out!")
- end
- ```
-
**Key insight:** Each process has its own memory, its own garbage collection.
One process can't corrupt another's state. If a process crashes, nothing else is affected.
@@ @@ -346,10 +323,10 @@ Counter.get_value(counter) |> IO.inspect(label: "After reset")
**Why this matters:**
- - `call` = **synchronous** — the caller waits. Like an HTTP request/response.
- - `cast` = **asynchronous** — fire and forget. Like sending an email.
- - Messages are processed **one at a time** — no race conditions, no locks, guaranteed.
- - The GenServer process is **isolated** — if it crashes, nothing else is affected.
+ * `call` = **synchronous** — the caller waits. Like an HTTP request/response.
+ * `cast` = **asynchronous** — fire and forget. Like sending an email.
+ * Messages are processed **one at a time** — no race conditions, no locks, guaranteed.
+ * The GenServer process is **isolated** — if it crashes, nothing else is affected.
**For JS devs:** This is like a Redux store, but it's a real concurrent process
that can handle requests from multiple callers simultaneously.
@@ @@ -454,7 +431,7 @@ rendered entirely on the server.
### How It Works
```
- ┌─────────────┐ ┌──────────────┐
+ ┌──────────────┐ ┌──────────────┐
│ Browser │ │ Server │
│ │ 1. HTTP GET │ │
│ │ ──────────────────>│ │
@@ @@ -464,14 +441,14 @@ rendered entirely on the server.
│ │ 2. WebSocket │ │
│ │ <════════════════> │ │
│ │ │ │
- │ User clicks │ 3. Event sent │ │
+ │ User clicks │ 3. Event sent │ │
│ a button │ ──────────────────>│ Processes │
│ │ │ the event, │
│ │ 4. HTML diff │ re-renders │
│ │ <──────────────────│ │
│ Browser │ │ │
│ patches DOM │ │ │
- └─────────────┘ └──────────────┘
+ └──────────────┘ └──────────────┘
```
1. First page load is **regular HTML** — fast, SEO-friendly
@@ @@ -523,15 +500,15 @@ end
With LiveView, you eliminate entire layers of your stack:
- | Traditional SPA | Phoenix LiveView |
- |------------------------------|-------------------------|
- | REST/GraphQL API | Not needed |
- | JSON serialization | Not needed |
- | Client-side state (Redux) | Server-side assigns |
- | API client (fetch/axios) | Not needed |
- | WebSocket library | Built in |
- | Client-side routing | Server-side routing |
- | JavaScript framework | Optional — only for JS-specific needs |
+ | Traditional SPA | Phoenix LiveView |
+ | ------------------------- | --------------------------------------- |
+ | REST/GraphQL API | Not needed |
+ | JSON serialization | Not needed |
+ | Client-side state (Redux) | Server-side assigns |
+ | API client (fetch/axios) | Not needed |
+ | WebSocket library | Built in |
+ | Client-side routing | Server-side routing |
+ | JavaScript framework | Optional — only for JS-specific needs |
The server is the source of truth. State lives in one place. Real-time features
like live search, live validation, presence tracking — they come nearly for free.
@@ @@ -540,10 +517,10 @@ like live search, live validation, presence tracking — they come nearly for fr
LiveView isn't anti-JavaScript. It has **hooks** for when you need client-side behavior:
- - Rich text editors (TinyMCE, Trix)
- - Maps and charts (Leaflet, Chart.js)
- - Drag and drop
- - Clipboard, camera, etc.
+ * Rich text editors (TinyMCE, Trix)
+ * Maps and charts (Leaflet, Chart.js)
+ * Drag and drop
+ * Clipboard, camera, etc.
But the business logic stays on the server.
@@ @@ -552,21 +529,24 @@ But the business logic stays on the server.
Three things that make Elixir different:
**Pattern Matching:**
- - `=` is matching, not assignment
- - Function heads replace if/else chains
- - `with` chains operations with built-in error handling
- - Your code reads like a specification
+
+ * `=` is matching, not assignment
+ * Function heads replace if/else chains
+ * `with` chains operations with built-in error handling
+ * Your code reads like a specification
**Processes & Concurrency:**
- - Lightweight processes (~2KB each, millions possible)
- - Message passing instead of shared state — no locks, no race conditions
- - GenServer for stateful processes
- - Supervisors automatically restart crashed processes
+
+ * Lightweight processes (~2KB each, millions possible)
+ * Message passing instead of shared state — no locks, no race conditions
+ * GenServer for stateful processes
+ * Supervisors automatically restart crashed processes
**LiveView:**
- - Real-time UI without writing JavaScript
- - Server renders HTML, sends only diffs over WebSocket
- - No API layer, no client state management, no JSON serialization
- - Built-in support for real-time features
+
+ * Real-time UI without writing JavaScript
+ * Server renders HTML, sends only diffs over WebSocket
+ * No API layer, no client state management, no JSON serialization
+ * Built-in support for real-time features
**Next up:** Let's see these patterns in production code...