slides/02-the-language.livemd

Torey Heinz committed Feb 23, 2026
commit ba547109ff9b8b121f8649270014cf6475673f95
Showing 2 changed files with 381 additions and 381 deletions
02-the-language.livemd +0 -381
@@ @@ -1,381 +0,0 @@
- # Part 2: The Language
-
- ## What Makes Elixir Click
-
- Just a couple of "aha" moments that make Elixir different.
-
- We'll cover three things:
-
- 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
-
- ## Pattern Matching: The Basics
-
- In most languages, `=` means **assignment**. In Elixir, `=` means **match**.
- It works like destructuring, but it also **validates the shape** of your data.
-
- ```elixir
- # Destructure a tuple
- {status, message} = {:ok, "Hello, GR Web Dev!"}
-
- IO.puts("Status: #{status}")
- IO.puts("Message: #{message}")
- ```
-
- ```elixir
- # Destructure a map — pull out just what you need
- user = %{name: "Torey", role: :developer, city: "Grand Rapids"}
-
- %{name: name, city: city} = user
-
- 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?
-
- {: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.
-
- **JavaScript:** Similar to destructuring — but it also
- validates the shape of your data at the same time.
-
- ## Pattern Matching: Function Heads
-
- 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
- # If we get an :ok tuple, celebrate
- def respond({:ok, data}) do
- "Success! Got: #{inspect(data)}"
- end
-
- # If we get an :error tuple, handle it
- def respond({:error, reason}) do
- "Something went wrong: #{reason}"
- end
-
- # Catch-all for anything else
- def respond(other) do
- "Unexpected: #{inspect(other)}"
- end
- end
- ```
-
- ```elixir
- Greeter.respond({:ok, [1, 2, 3]})
- ```
-
- ```elixir
- Greeter.respond({:error, "not found"})
- ```
-
- ```elixir
- Greeter.respond("something random")
- ```
-
- No `if/else`. No `switch`. Each function clause handles **one case**, and the code
- reads like a specification.
-
- ## Pattern Matching: A Practical Example
-
- Here's something closer to real code. Imagine handling different shapes of API responses:
-
- ```elixir
- defmodule ApiHandler do
- # Success with data
- def handle_response(%{status: 200, body: body}) do
- {:ok, body}
- end
-
- # Created
- def handle_response(%{status: 201, body: body}) do
- {:ok, body}
- end
-
- # Not found
- def handle_response(%{status: 404}) do
- {:error, :not_found}
- end
-
- # Rate limited — note the header extraction
- def handle_response(%{status: 429, headers: headers}) do
- retry_after = Map.get(headers, "retry-after", "60")
- {:rate_limited, String.to_integer(retry_after)}
- end
-
- # Server error
- def handle_response(%{status: status}) when status >= 500 do
- {:error, :server_error}
- end
-
- # Anything else
- def handle_response(%{status: status}) do
- {:error, {:unexpected_status, status}}
- end
- end
- ```
-
- ```elixir
- # Try different responses:
- ApiHandler.handle_response(%{status: 200, body: %{users: ["Alice", "Bob"]}})
- ```
-
- ```elixir
- ApiHandler.handle_response(%{status: 429, headers: %{"retry-after" => "30"}})
- ```
-
- No `if/else` chains, no `switch` statements. Each clause is self-documenting.
-
- ## Pattern Matching: The `with` Statement
-
- Real-world code often has multiple steps that can each fail. In other languages there's an Interactor pattern that often requires a whole library to implement. Elixir has the built-in `with` statement
-
- ```elixir
- defmodule OrderProcessor do
- def process_order(order_id) do
- with(
- {:ok, order} <- find_order(order_id),
- {:ok, _} <- validate_inventory(order),
- {:ok, charge} <- charge_payment(order),
- {:ok, shipment} <- create_shipment(order)
- ) do
- {:ok, %{order: order, charge: charge, shipment: shipment}}
- else
- {:error, :not_found} -> {:error, "Order not found"}
- {:error, :out_of_stock} -> {:error, "Item is out of stock"}
- {:error, :payment_declined} -> {:error, "Payment was declined"}
- {:error, reason} -> {:error, "Unexpected error: #{inspect(reason)}"}
- end
- end
-
- # 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}) 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
- ```
-
- ```elixir
- # Happy path — all steps succeed
- OrderProcessor.process_order(1)
- ```
-
- ```elixir
- # Out of stock — short circuits at validate_inventory
- OrderProcessor.process_order(2)
- ```
-
- The `with` statement chains operations that return `{:ok, value}` or `{:error, reason}`.
- If any step fails, it jumps straight to the `else` block.
-
- ## Processes: The Heart of Elixir
-
- Everything in Elixir runs inside **processes** — not OS processes, not threads.
- They're ~2KB each, completely isolated, with their own memory and garbage collection.
-
- ```elixir
- # Spawn a process — it runs concurrently
- spawn(fn ->
- IO.puts("Hello from process #{inspect(self())}")
- end)
-
- IO.puts("Hello from the main process #{inspect(self())}")
- ```
-
- ```elixir
- # Let's spawn 10,000 just to show it's trivial.
- {time_microseconds, _} =
- :timer.tc(fn ->
- 1..10_000
- |> Enum.map(fn i ->
- spawn(fn -> i * i end)
- end)
- end)
-
- IO.puts("Spawned 10,000 processes in #{time_microseconds / 1_000}ms")
- ```
-
- ## Processes: Supervisors — "Let It Crash"
-
- What happens when a process crashes? In most languages, you write defensive code
- to prevent every possible error. In Elixir, you **let it crash** and have a
- Supervisor automatically restart it.
-
- ```elixir
- defmodule UnreliableWorker do
- use GenServer
-
- def start_link(opts) do
- name = Keyword.get(opts, :name, __MODULE__)
- GenServer.start_link(__MODULE__, opts, name: name)
- end
-
- def get_count(pid), do: GenServer.call(pid, :get_count)
- def do_work(pid), do: GenServer.call(pid, :do_work)
-
- @impl true
- def init(_opts) do
- IO.puts(" [Worker] Started! (PID: #{inspect(self())})")
- {:ok, %{count: 0}}
- end
-
- @impl true
- def handle_call(:get_count, _from, state) do
- {:reply, state.count, state}
- end
-
- @impl true
- def handle_call(:do_work, _from, state) do
- new_count = state.count + 1
-
- if new_count >= 3 do
- # Simulate a crash on the 3rd call
- raise "Boom! Something went wrong on call ##{new_count}"
- end
-
- IO.puts(" [Worker] Completed work ##{new_count}")
- {:reply, {:ok, new_count}, %{state | count: new_count}}
- end
- end
- ```
-
- ```elixir
- # Start a Supervisor watching our unreliable worker
- children = [
- {UnreliableWorker, name: :my_worker}
- ]
-
- {:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_one)
-
- IO.puts("--- Doing work ---")
- UnreliableWorker.do_work(:my_worker) |> IO.inspect(label: "Call 1")
- UnreliableWorker.do_work(:my_worker) |> IO.inspect(label: "Call 2")
-
- IO.puts("\n--- This next call will crash the worker ---")
-
- try do
- UnreliableWorker.do_work(:my_worker)
- catch
- :exit, _ -> IO.puts(" [Caller] The worker crashed!")
- end
-
- # Give the supervisor a moment to restart the worker
- Process.sleep(100)
-
- IO.puts("\n--- Worker was automatically restarted by the Supervisor ---")
- IO.puts("--- Notice: it's a NEW process with fresh state ---")
- UnreliableWorker.do_work(:my_worker) |> IO.inspect(label: "Call 1 (restarted)")
- UnreliableWorker.get_count(:my_worker) |> IO.inspect(label: "Count")
- ```
-
- **What just happened:**
-
- 1. The worker crashed on the 3rd call
- 2. The Supervisor detected the crash
- 3. The Supervisor started a **brand new** worker automatically
- 4. The new worker has fresh state (count = 0)
- 5. Everything keeps running — the crash was isolated and recovered
-
- This is the **"let it crash"** philosophy. Instead of writing defensive code for every
- possible failure, you design for recovery. Your Supervisor is like a manager who
- automatically fixes problems instead of you writing code to prevent every possible issue.
-
- **In production at Vianet:** Our email rate limiter, database connections, TCP connections
- to Verisign — they're all supervised. If any of them crash, they restart automatically.
- The application keeps running.
-
- ## LiveView: Real-Time Web Without JavaScript
-
- LiveView is a Phoenix framework feature that gives you **real-time, interactive UIs**
- rendered entirely on the server.
-
- ### How It Works
-
- ```
- ┌──────────────┐ ┌──────────────┐
- │ Browser │ │ Server │
- │ │ 1. HTTP GET │ │
- │ │ ──────────────────>│ │
- │ │ Full HTML page │ │
- │ │ <──────────────────│ │
- │ │ │ │
- │ │ 2. WebSocket │ │
- │ │ <════════════════> │ │
- │ │ │ │
- │ 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
- 2. WebSocket connection upgrades the page to real-time
- 3. User interactions send **tiny events** over the WebSocket
- 4. Server re-renders and sends back **only the HTML that changed**
-
- ### What the Code Looks Like
-
- Here's a simplified version of a real search feature from one of our apps:
-
- <!-- livebook:{"force_markdown":true} -->
-
- ```elixir
- defmodule MyAppWeb.SearchLive do
- use MyAppWeb, :live_view
-
- def mount(_params, _session, socket) do
- {:ok,
- socket
- |> assign(:query, "")
- |> assign(:results, [])
- end
-
- # User types in the search box
- # Search runs asynchronously
- def handle_event("search", %{"query" => query}, socket) do
- results = MyApp.Search.find(query)
-
- {:noreply,
- socket
- |> assign(:results, results)
- end
- end
- ```
-
- ## Recap
-
- * **Pattern matching** — function heads replace if/else, `with` chains operations with error handling
- * **Processes** — lightweight (~2KB), isolated, supervised — "let it crash" instead of defensive code
- * **LiveView** — real-time UI without JavaScript, server renders HTML diffs over WebSocket
-
- **Next up:** Let's see these patterns in production code...
slides/02-the-language.livemd +381 -0
@@ @@ -0,0 +1,381 @@
+ # Part 2: The Language
+
+ ## What Makes Elixir Click
+
+ Just a couple of "aha" moments that make Elixir different.
+
+ We'll cover three things:
+
+ 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
+
+ ## Pattern Matching: The Basics
+
+ In most languages, `=` means **assignment**. In Elixir, `=` means **match**.
+ It works like destructuring, but it also **validates the shape** of your data.
+
+ ```elixir
+ # Destructure a tuple
+ {status, message} = {:ok, "Hello, GR Web Dev!"}
+
+ IO.puts("Status: #{status}")
+ IO.puts("Message: #{message}")
+ ```
+
+ ```elixir
+ # Destructure a map — pull out just what you need
+ user = %{name: "Torey", role: :developer, city: "Grand Rapids"}
+
+ %{name: name, city: city} = user
+
+ 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?
+
+ {: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.
+
+ **JavaScript:** Similar to destructuring — but it also
+ validates the shape of your data at the same time.
+
+ ## Pattern Matching: Function Heads
+
+ 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
+ # If we get an :ok tuple, celebrate
+ def respond({:ok, data}) do
+ "Success! Got: #{inspect(data)}"
+ end
+
+ # If we get an :error tuple, handle it
+ def respond({:error, reason}) do
+ "Something went wrong: #{reason}"
+ end
+
+ # Catch-all for anything else
+ def respond(other) do
+ "Unexpected: #{inspect(other)}"
+ end
+ end
+ ```
+
+ ```elixir
+ Greeter.respond({:ok, [1, 2, 3]})
+ ```
+
+ ```elixir
+ Greeter.respond({:error, "not found"})
+ ```
+
+ ```elixir
+ Greeter.respond("something random")
+ ```
+
+ No `if/else`. No `switch`. Each function clause handles **one case**, and the code
+ reads like a specification.
+
+ ## Pattern Matching: A Practical Example
+
+ Here's something closer to real code. Imagine handling different shapes of API responses:
+
+ ```elixir
+ defmodule ApiHandler do
+ # Success with data
+ def handle_response(%{status: 200, body: body}) do
+ {:ok, body}
+ end
+
+ # Created
+ def handle_response(%{status: 201, body: body}) do
+ {:ok, body}
+ end
+
+ # Not found
+ def handle_response(%{status: 404}) do
+ {:error, :not_found}
+ end
+
+ # Rate limited — note the header extraction
+ def handle_response(%{status: 429, headers: headers}) do
+ retry_after = Map.get(headers, "retry-after", "60")
+ {:rate_limited, String.to_integer(retry_after)}
+ end
+
+ # Server error
+ def handle_response(%{status: status}) when status >= 500 do
+ {:error, :server_error}
+ end
+
+ # Anything else
+ def handle_response(%{status: status}) do
+ {:error, {:unexpected_status, status}}
+ end
+ end
+ ```
+
+ ```elixir
+ # Try different responses:
+ ApiHandler.handle_response(%{status: 200, body: %{users: ["Alice", "Bob"]}})
+ ```
+
+ ```elixir
+ ApiHandler.handle_response(%{status: 429, headers: %{"retry-after" => "30"}})
+ ```
+
+ No `if/else` chains, no `switch` statements. Each clause is self-documenting.
+
+ ## Pattern Matching: The `with` Statement
+
+ Real-world code often has multiple steps that can each fail. In other languages there's an Interactor pattern that often requires a whole library to implement. Elixir has the built-in `with` statement
+
+ ```elixir
+ defmodule OrderProcessor do
+ def process_order(order_id) do
+ with(
+ {:ok, order} <- find_order(order_id),
+ {:ok, _} <- validate_inventory(order),
+ {:ok, charge} <- charge_payment(order),
+ {:ok, shipment} <- create_shipment(order)
+ ) do
+ {:ok, %{order: order, charge: charge, shipment: shipment}}
+ else
+ {:error, :not_found} -> {:error, "Order not found"}
+ {:error, :out_of_stock} -> {:error, "Item is out of stock"}
+ {:error, :payment_declined} -> {:error, "Payment was declined"}
+ {:error, reason} -> {:error, "Unexpected error: #{inspect(reason)}"}
+ end
+ end
+
+ # 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}) 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
+ ```
+
+ ```elixir
+ # Happy path — all steps succeed
+ OrderProcessor.process_order(1)
+ ```
+
+ ```elixir
+ # Out of stock — short circuits at validate_inventory
+ OrderProcessor.process_order(2)
+ ```
+
+ The `with` statement chains operations that return `{:ok, value}` or `{:error, reason}`.
+ If any step fails, it jumps straight to the `else` block.
+
+ ## Processes: The Heart of Elixir
+
+ Everything in Elixir runs inside **processes** — not OS processes, not threads.
+ They're ~2KB each, completely isolated, with their own memory and garbage collection.
+
+ ```elixir
+ # Spawn a process — it runs concurrently
+ spawn(fn ->
+ IO.puts("Hello from process #{inspect(self())}")
+ end)
+
+ IO.puts("Hello from the main process #{inspect(self())}")
+ ```
+
+ ```elixir
+ # Let's spawn 10,000 just to show it's trivial.
+ {time_microseconds, _} =
+ :timer.tc(fn ->
+ 1..10_000
+ |> Enum.map(fn i ->
+ spawn(fn -> i * i end)
+ end)
+ end)
+
+ IO.puts("Spawned 10,000 processes in #{time_microseconds / 1_000}ms")
+ ```
+
+ ## Processes: Supervisors — "Let It Crash"
+
+ What happens when a process crashes? In most languages, you write defensive code
+ to prevent every possible error. In Elixir, you **let it crash** and have a
+ Supervisor automatically restart it.
+
+ ```elixir
+ defmodule UnreliableWorker do
+ use GenServer
+
+ def start_link(opts) do
+ name = Keyword.get(opts, :name, __MODULE__)
+ GenServer.start_link(__MODULE__, opts, name: name)
+ end
+
+ def get_count(pid), do: GenServer.call(pid, :get_count)
+ def do_work(pid), do: GenServer.call(pid, :do_work)
+
+ @impl true
+ def init(_opts) do
+ IO.puts(" [Worker] Started! (PID: #{inspect(self())})")
+ {:ok, %{count: 0}}
+ end
+
+ @impl true
+ def handle_call(:get_count, _from, state) do
+ {:reply, state.count, state}
+ end
+
+ @impl true
+ def handle_call(:do_work, _from, state) do
+ new_count = state.count + 1
+
+ if new_count >= 3 do
+ # Simulate a crash on the 3rd call
+ raise "Boom! Something went wrong on call ##{new_count}"
+ end
+
+ IO.puts(" [Worker] Completed work ##{new_count}")
+ {:reply, {:ok, new_count}, %{state | count: new_count}}
+ end
+ end
+ ```
+
+ ```elixir
+ # Start a Supervisor watching our unreliable worker
+ children = [
+ {UnreliableWorker, name: :my_worker}
+ ]
+
+ {:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_one)
+
+ IO.puts("--- Doing work ---")
+ UnreliableWorker.do_work(:my_worker) |> IO.inspect(label: "Call 1")
+ UnreliableWorker.do_work(:my_worker) |> IO.inspect(label: "Call 2")
+
+ IO.puts("\n--- This next call will crash the worker ---")
+
+ try do
+ UnreliableWorker.do_work(:my_worker)
+ catch
+ :exit, _ -> IO.puts(" [Caller] The worker crashed!")
+ end
+
+ # Give the supervisor a moment to restart the worker
+ Process.sleep(100)
+
+ IO.puts("\n--- Worker was automatically restarted by the Supervisor ---")
+ IO.puts("--- Notice: it's a NEW process with fresh state ---")
+ UnreliableWorker.do_work(:my_worker) |> IO.inspect(label: "Call 1 (restarted)")
+ UnreliableWorker.get_count(:my_worker) |> IO.inspect(label: "Count")
+ ```
+
+ **What just happened:**
+
+ 1. The worker crashed on the 3rd call
+ 2. The Supervisor detected the crash
+ 3. The Supervisor started a **brand new** worker automatically
+ 4. The new worker has fresh state (count = 0)
+ 5. Everything keeps running — the crash was isolated and recovered
+
+ This is the **"let it crash"** philosophy. Instead of writing defensive code for every
+ possible failure, you design for recovery. Your Supervisor is like a manager who
+ automatically fixes problems instead of you writing code to prevent every possible issue.
+
+ **In production at Vianet:** Our email rate limiter, database connections, TCP connections
+ to Verisign — they're all supervised. If any of them crash, they restart automatically.
+ The application keeps running.
+
+ ## LiveView: Real-Time Web Without JavaScript
+
+ LiveView is a Phoenix framework feature that gives you **real-time, interactive UIs**
+ rendered entirely on the server.
+
+ ### How It Works
+
+ ```
+ ┌──────────────┐ ┌──────────────┐
+ │ Browser │ │ Server │
+ │ │ 1. HTTP GET │ │
+ │ │ ──────────────────>│ │
+ │ │ Full HTML page │ │
+ │ │ <──────────────────│ │
+ │ │ │ │
+ │ │ 2. WebSocket │ │
+ │ │ <════════════════> │ │
+ │ │ │ │
+ │ 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
+ 2. WebSocket connection upgrades the page to real-time
+ 3. User interactions send **tiny events** over the WebSocket
+ 4. Server re-renders and sends back **only the HTML that changed**
+
+ ### What the Code Looks Like
+
+ Here's a simplified version of a real search feature from one of our apps:
+
+ <!-- livebook:{"force_markdown":true} -->
+
+ ```elixir
+ defmodule MyAppWeb.SearchLive do
+ use MyAppWeb, :live_view
+
+ def mount(_params, _session, socket) do
+ {:ok,
+ socket
+ |> assign(:query, "")
+ |> assign(:results, [])
+ end
+
+ # User types in the search box
+ # Search runs asynchronously
+ def handle_event("search", %{"query" => query}, socket) do
+ results = MyApp.Search.find(query)
+
+ {:noreply,
+ socket
+ |> assign(:results, results)
+ end
+ end
+ ```
+
+ ## Recap
+
+ * **Pattern matching** — function heads replace if/else, `with` chains operations with error handling
+ * **Processes** — lightweight (~2KB), isolated, supervised — "let it crash" instead of defensive code
+ * **LiveView** — real-time UI without JavaScript, server renders HTML diffs over WebSocket
+
+ **Next up:** Let's see these patterns in production code...