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... | |