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