# Book 1 — Elixir Fast-Track & the BEAM

```elixir
Mix.install([
  {:kino, "~> 0.14"},
  {:pythonx, "~> 0.4.2"},
  {:kino_pythonx, "~> 0.1.0"}
])
```

## How to use this notebook

Read `book1-elixir-and-beam.html` alongside this notebook. Each section here is hands-on practice for the matching section there. Run every cell (`Ctrl/Cmd+Enter` or the ▶ button), then try the **exercises** — they have hidden solutions below them.

Livebook itself is Elixir: every cell runs in a real BEAM node, so we can spawn processes, inspect the VM, and crash things safely.

## 1. Syntax speedrun

```elixir
# Everything is an expression. The last expression in a cell is its result.
defmodule Greeter do
  def hello(name, greeting \\ "Hello") do
    "#{greeting}, #{name}!"
  end
end

Greeter.hello("Livebook")
```

```elixir
# The core data types, in one cell:
atom = :ok
tuple = {:ok, 42}
list = [1, 2, 3]
map = %{name: "Ada", age: 36}
anon_fn = fn x -> x * 2 end

{atom, tuple, hd(list), map.name, anon_fn.(21)}
```

```elixir
# Immutability: "updating" returns a new value
user = %{name: "Ada", age: 36}
older = Map.put(user, :age, 37)

# the original is untouched:
{user.age, older.age}
```

### Exercise 1.1

Write a module `Temp` with a function `to_f/1` that converts Celsius to Fahrenheit (`c * 9 / 5 + 32`), and give it a second clause `to_f/1` that returns `{:error, :not_a_number}` when given anything that isn't a number. Hint: guard clauses — `when is_number(c)`.

```elixir
# your code here
```

<details>
<summary>💡 Solution</summary>

```elixir
defmodule Temp do
  def to_f(c) when is_number(c), do: c * 9 / 5 + 32
  def to_f(_), do: {:error, :not_a_number}
end

{Temp.to_f(100), Temp.to_f("hot")}
```

</details>

## 2. Pattern matching

```elixir
# = is a match, not an assignment
{:ok, value} = {:ok, "it worked"}
value
```

```elixir
# When a match fails, it raises. Run this and read the error — you'll see it constantly:
{:ok, value} = {:error, :nope}
```

```elixir
# Lists destructure head/tail; maps match partially
[first | rest] = [1, 2, 3, 4]
%{name: n} = %{name: "Ada", age: 36, city: "London"}

{first, rest, n}
```

```elixir
# case is pattern matching inline. The ^ "pin" matches against an existing value
# instead of rebinding:
expected = 200

response = {:ok, %{status: 200, body: "<html>..."}}

case response do
  {:ok, %{status: ^expected, body: body}} -> {:success, byte_size(body)}
  {:ok, %{status: status}} -> {:unexpected_status, status}
  {:error, reason} -> {:failed, reason}
end
```

### Exercise 1.2

Write `Describe.shape/1` using **only function clauses** (no `if`, no `case`) that returns:

* `"circle of radius R"` for `{:circle, r}`
* `"WxH rectangle"` for `{:rect, w, h}`
* `"square"` for `{:rect, s, s}` (both sides equal — order of clauses matters!)

```elixir
# your code here — then test:
# Describe.shape({:rect, 3, 3}) should be "square"
```

<details>
<summary>💡 Solution</summary>

```elixir
defmodule Describe do
  def shape({:circle, r}), do: "circle of radius #{r}"
  def shape({:rect, s, s}), do: "square"          # must come BEFORE the general rect clause
  def shape({:rect, w, h}), do: "#{w}x#{h} rectangle"
end

{Describe.shape({:circle, 2}), Describe.shape({:rect, 3, 3}), Describe.shape({:rect, 3, 4})}
```

Note the `{:rect, s, s}` trick: using the same variable twice in a pattern requires both positions to be equal.

</details>

## 3. Data transformation and pipes

```elixir
"the quick brown fox jumps over the lazy dog"
|> String.split()
|> Enum.map(&String.capitalize/1)
|> Enum.filter(&(String.length(&1) > 3))
|> Enum.join(" ")
```

```elixir
# Enum.reduce is the universal tool (your Array.reduce / functools.reduce):
1..10
|> Enum.reduce(%{evens: 0, odds: 0}, fn n, acc ->
  key = if rem(n, 2) == 0, do: :evens, else: :odds
  Map.update!(acc, key, &(&1 + 1))
end)
```

```elixir
# Streams are lazy — nothing runs until Enum forces it:
1..1_000_000_000
|> Stream.map(&(&1 * 3))
|> Stream.filter(&(rem(&1, 2) == 0))
|> Enum.take(5)
```

### Exercise 1.3

Given the orders below, produce a map of `customer => total spent`, sorted... well, maps aren't sorted — produce a **list of `{customer, total}` tuples, highest total first**. Useful: `Enum.group_by/2`, `Enum.map/2`, `Enum.sort_by/3`.

```elixir
orders = [
  %{customer: "ada", amount: 30},
  %{customer: "grace", amount: 60},
  %{customer: "ada", amount: 25},
  %{customer: "alan", amount: 10},
  %{customer: "grace", amount: 5}
]

# your pipeline here
```

<details>
<summary>💡 Solution</summary>

```elixir
orders
|> Enum.group_by(& &1.customer)
|> Enum.map(fn {customer, list} ->
  {customer, list |> Enum.map(& &1.amount) |> Enum.sum()}
end)
|> Enum.sort_by(fn {_c, total} -> total end, :desc)
```

</details>

## 4. Meet the BEAM

You're inside it right now. Ask it about itself:

```elixir
%{
  schedulers_online: System.schedulers_online(),
  process_count: length(Process.list()),
  process_limit: :erlang.system_info(:process_limit),
  otp_release: System.otp_release(),
  elixir: System.version()
}
```

```elixir
# How cheap is a process? Let's spawn 100,000 and measure.
{micros, _} =
  :timer.tc(fn ->
    for _ <- 1..100_000 do
      spawn(fn -> :ok end)
    end
  end)

"spawned 100k processes in #{micros / 1000} ms (#{Float.round(micros / 100_000, 2)} µs each)"
```

```elixir
# Preemption demo: an infinite loop in one process does NOT freeze this notebook.
hog = spawn(fn -> Stream.iterate(0, &(&1 + 1)) |> Enum.each(fn _ -> :ok end) end)

# The scheduler keeps everything else responsive. Prove it:
Process.sleep(100)
result = 1 + 1

# clean up the hog and show we stayed alive:
Process.exit(hog, :kill)
"still responsive, 1 + 1 = #{result} (try this in Node!)"
```

## 5. spawn, send, receive

```elixir
# A one-shot echo process
pid =
  spawn(fn ->
    receive do
      {:ping, from} -> send(from, {:pong, self()})
    end
  end)

send(pid, {:ping, self()})

receive do
  {:pong, from} -> "got pong from #{inspect(from)}"
after
  1000 -> "timeout!"
end
```

```elixir
# State via recursion: the pattern under ALL of OTP
defmodule Counter do
  def loop(count) do
    receive do
      :increment -> loop(count + 1)
      {:get, caller} ->
        send(caller, {:count, count})
        loop(count)
    end
  end
end

counter = spawn(Counter, :loop, [0])

send(counter, :increment)
send(counter, :increment)
send(counter, :increment)
send(counter, {:get, self()})

receive do
  {:count, n} -> "count is #{n}"
end
```

```elixir
# Peek inside a process: mailbox size, memory, current function
counter |> Process.info([:message_queue_len, :memory, :status])
```

### Exercise 1.4

Build a `KV.loop/1` process holding a map, that understands two messages:

* `{:put, key, value}` — store the pair
* `{:get, key, caller}` — send `{:value, value_or_nil}` back to `caller`

```elixir
# your code here — then exercise it with send/receive
```

<details>
<summary>💡 Solution</summary>

```elixir
defmodule KV do
  def loop(state) do
    receive do
      {:put, key, value} ->
        loop(Map.put(state, key, value))

      {:get, key, caller} ->
        send(caller, {:value, Map.get(state, key)})
        loop(state)
    end
  end
end

kv = spawn(KV, :loop, [%{}])
send(kv, {:put, :lang, "elixir"})
send(kv, {:get, :lang, self()})

receive do
  {:value, v} -> v
end
```

You just wrote a tiny Redis. In Book 2, GenServer replaces this boilerplate.

</details>

## 6. Links, monitors, and crashes

```elixir
# A crash in a plain spawn is contained — the notebook survives:
spawn(fn -> raise "boom (contained)" end)
Process.sleep(50)
"the crash above was logged, but this cell still ran"
```

```elixir
# A monitor turns another process's death into a message:
{pid, ref} = spawn_monitor(fn -> raise "boom (monitored)" end)

receive do
  {:DOWN, ^ref, :process, ^pid, reason} ->
    "observed the crash as data: #{inspect(reason)}"
end
```

```elixir
# A link propagates the crash — but we can trap exits to convert it to a message.
# (This is exactly what supervisors do.)
Process.flag(:trap_exit, true)

pid = spawn_link(fn -> raise "boom (linked)" end)

receive do
  {:EXIT, ^pid, reason} -> "trapped exit: #{inspect(reason)}"
end
```

```elixir
# A hand-rolled "supervisor": restart the worker whenever it dies.
defmodule NaiveSupervisor do
  def start(worker_fun) do
    Process.flag(:trap_exit, true)
    supervise(worker_fun, 0)
  end

  defp supervise(worker_fun, restarts) when restarts < 3 do
    pid = spawn_link(worker_fun)
    IO.puts("supervisor: started worker #{inspect(pid)} (restart ##{restarts})")

    receive do
      {:EXIT, ^pid, reason} ->
        IO.puts("supervisor: worker died (#{inspect(reason)}), restarting...")
        supervise(worker_fun, restarts + 1)
    end
  end

  defp supervise(_fun, _restarts), do: IO.puts("supervisor: too many restarts, giving up")
end

spawn(fn ->
  NaiveSupervisor.start(fn ->
    Process.sleep(Enum.random(100..300))
    raise "worker crashed!"
  end)
end)

Process.sleep(2000)
"watch the log output above — that's 'let it crash' in miniature"
```

## Wrap-up

You now have the whole foundation: immutable data transformed by pattern-matched functions, running in cheap isolated processes that communicate by message and are restarted when they die.

**Next:** `book2-otp-core.livemd`, where GenServer and Supervisor replace all the hand-rolled loops you wrote today.
