Skip to content

Commit ce64fc3

Browse files
authored
Revamp Mix & OTP guides (#14637)
1 parent be9352e commit ce64fc3

16 files changed

+1341
-2157
lines changed

lib/elixir/pages/getting-started/debugging.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ dbg(Map.put(feature, :in_version, "1.14.0"))
7777

7878
The code above prints this:
7979

80-
```shell
80+
```text
8181
[my_file.exs:2: (file)]
8282
feature #=> %{inspiration: "Rust", name: :dbg}
8383
[my_file.exs:3: (file)]
@@ -97,7 +97,7 @@ __ENV__.file
9797

9898
This code prints:
9999

100-
```shell
100+
```text
101101
[dbg_pipes.exs:5: (file)]
102102
__ENV__.file #=> "/home/myuser/dbg_pipes.exs"
103103
|> String.split("/", trim: true) #=> ["home", "myuser", "dbg_pipes.exs"]

lib/elixir/pages/mix-and-otp/agents.md

Lines changed: 31 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,22 @@
33
SPDX-FileCopyrightText: 2021 The Elixir Team
44
-->
55

6-
# Simple state management with agents
6+
# Simple state with agents
77

88
In this chapter, we will learn how to keep and share state between multiple entities. If you have previous programming experience, you may think of globally shared variables, but the model we will learn here is quite different. The next chapters will generalize the concepts introduced here.
99

1010
If you have skipped the *Getting Started* guide or read it long ago, be sure to re-read the [Processes](../getting-started/processes.md) chapter. We will use it as a starting point.
1111

1212
## The trouble with (mutable) state
1313

14-
Elixir is an immutable language where nothing is shared by default. If we want to share information, which can be read and modified from multiple places, we have two main options in Elixir:
14+
Elixir is an immutable language where nothing is shared by default. If we want to share information, this is typically done by sending messages between processes.
1515

16-
* Using processes and message passing
17-
* [ETS (Erlang Term Storage)](`:ets`)
18-
19-
We covered processes in the *Getting Started* guide. ETS (Erlang Term Storage) is a new topic that we will explore in later chapters. When it comes to processes though, we rarely hand-roll our own, instead we use the abstractions available in Elixir and OTP:
16+
When it comes to processes though, we rarely hand-roll our own, instead we use the abstractions available in Elixir and OTP:
2017

2118
* `Agent` — Simple wrappers around state.
2219
* `GenServer` — "Generic servers" (processes) that encapsulate state, provide sync and async calls, support code reloading, and more.
2320
* `Task` — Asynchronous units of computation that allow spawning a process and potentially retrieving its result at a later time.
2421

25-
We will explore these abstractions as we move forward. Keep in mind that they are all implemented on top of processes using the basic features provided by the VM, like `send/2`, `receive/1`, `spawn/1` and `Process.link/1`.
26-
2722
Here, we will use agents, and create a module named `KV.Bucket`, responsible for storing our key-value entries in a way that allows them to be read and modified by other processes.
2823

2924
## Agents 101
@@ -47,7 +42,7 @@ iex> Agent.stop(agent)
4742
:ok
4843
```
4944

50-
We started an agent with an initial state of an empty list. We updated the agent's state, adding our new item to the head of the list. The second argument of `Agent.update/3` is a function that takes the agent's current state as input and returns its desired new state. Finally, we retrieved the whole list. The second argument of `Agent.get/3` is a function that takes the state as input and returns the value that `Agent.get/3` itself will return. Once we are done with the agent, we can call `Agent.stop/3` to terminate the agent process.
45+
We started an agent with an initial state of an empty list. The `start_link/1` function returned the `:ok` tuple with a process identifier (PID) of the agent. We will use this PID for all further interactions. We then updated the agent's state, adding our new item to the head of the list. The second argument of `Agent.update/3` is a function that takes the agent's current state as input and returns its desired new state. Finally, we retrieved the whole list. The second argument of `Agent.get/3` is a function that takes the state as input and returns the value that `Agent.get/3` itself will return. Once we are done with the agent, we can call `Agent.stop/3` to terminate the agent process.
5146

5247
The `Agent.update/3` function accepts as a second argument any function that receives one argument and returns a value:
5348

@@ -93,7 +88,9 @@ Also note the `async: true` option passed to `ExUnit.Case`. This option makes th
9388
Async or not, our new test should obviously fail, as none of the functionality is implemented in the module being tested:
9489

9590
```text
96-
** (UndefinedFunctionError) function KV.Bucket.start_link/1 is undefined (module KV.Bucket is not available)
91+
1) test stores values by key (KV.BucketTest)
92+
test/kv/bucket_test.exs:4
93+
** (UndefinedFunctionError) function KV.Bucket.start_link/1 is undefined (module KV.Bucket is not available)
9794
```
9895

9996
In order to fix the failing test, let's create a file at `lib/kv/bucket.ex` with the contents below. Feel free to give a try at implementing the `KV.Bucket` module yourself using agents before peeking at the implementation below.
@@ -104,9 +101,11 @@ defmodule KV.Bucket do
104101

105102
@doc """
106103
Starts a new bucket.
104+
105+
All options are forwarded to `Agent.start_link/2`.
107106
"""
108-
def start_link(_opts) do
109-
Agent.start_link(fn -> %{} end)
107+
def start_link(opts) do
108+
Agent.start_link(fn -> %{} end, opts)
110109
end
111110

112111
@doc """
@@ -125,49 +124,43 @@ defmodule KV.Bucket do
125124
end
126125
```
127126

128-
The first step in our implementation is to call `use Agent`. Most of the functionality we will learn, such as `GenServer` and `Supervisor`, follow this pattern. For all of them, calling `use` generates a `child_spec/1` function with default configuration, which will be handy when we start supervising processes in chapter 4.
127+
The first step in our implementation is to call `use Agent`. This is a pattern we will see throughout the guides and understand in depth in the next chapter.
129128

130-
Then we define a `start_link/1` function, which will effectively start the agent. It is a convention to define a `start_link/1` function that always accepts a list of options. We don't plan on using any options right now, but we might later on. We then proceed to call `Agent.start_link/1`, which receives an anonymous function that returns the Agent's initial state.
129+
Then we define a `start_link/1` function, which will effectively start the agent. It is a convention to define a `start_link/1` function that always accepts a list of options. We then call `Agent.start_link/2` passing an anonymous function that returns the Agent's initial state and the same list of options we received.
131130

132131
We are keeping a map inside the agent to store our keys and values. Getting and putting values on the map is done with the Agent API and the capture operator `&`, introduced in [the Getting Started guide](../getting-started/anonymous-functions.md#the-capture-operator). The agent passes its state to the anonymous function via the `&1` argument when `Agent.get/2` and `Agent.update/2` are called.
133132

134133
Now that the `KV.Bucket` module has been defined, our test should pass! You can try it yourself by running: `mix test`.
135134

136-
## Test setup with ExUnit callbacks
135+
## Naming processes
137136

138-
Before moving on and adding more features to `KV.Bucket`, let's talk about ExUnit callbacks. As you may expect, all `KV.Bucket` tests will require a bucket agent to be up and running. Luckily, ExUnit supports callbacks that allow us to skip such repetitive tasks.
137+
When starting `KV.Bucket`, we pass a list of options which we forward to `Agent.start_link/2`. One of the options accepted by `Agent.start_link/2` is a name option which allows us to name a process, so we can interact with it using its name instead of its PID.
139138

140-
Let's rewrite the test case to use callbacks:
139+
Let's write a test as an example. Back on `KV.BucketTest`, add this:
141140

142141
```elixir
143-
defmodule KV.BucketTest do
144-
use ExUnit.Case, async: true
142+
test "stores values by key on a named process" do
143+
{:ok, _} = KV.Bucket.start_link(name: :shopping_list)
144+
assert KV.Bucket.get(:shopping_list, "milk") == nil
145145

146-
setup do
147-
{:ok, bucket} = KV.Bucket.start_link([])
148-
%{bucket: bucket}
146+
KV.Bucket.put(:shopping_list, "milk", 3)
147+
assert KV.Bucket.get(:shopping_list, "milk") == 3
149148
end
150-
151-
test "stores values by key", %{bucket: bucket} do
152-
assert KV.Bucket.get(bucket, "milk") == nil
153-
154-
KV.Bucket.put(bucket, "milk", 3)
155-
assert KV.Bucket.get(bucket, "milk") == 3
156-
end
157-
end
158149
```
159150

160-
We have first defined a setup callback with the help of the `setup/1` macro. The `setup/1` macro defines a callback that is run before every test, in the same process as the test itself.
161-
162-
Note that we need a mechanism to pass the `bucket` PID from the callback to the test. We do so by using the *test context*. When we return `%{bucket: bucket}` from the callback, ExUnit will merge this map into the test context. Since the test context is a map itself, we can pattern match the bucket out of it, providing access to the bucket inside the test:
151+
However, keep in mind that names are shared in the current node. If two tests attempt to create two processes named `:shopping_list` at the same time, one would succeed and the other would fail. For this reason, it is a common practice in Elixir to name processes started during tests after the test itself, like this:
163152

164153
```elixir
165-
test "stores values by key", %{bucket: bucket} do
166-
# `bucket` is now the bucket from the setup block
167-
end
154+
test "stores values by key on a named process", config do
155+
{:ok, _} = KV.Bucket.start_link(name: config.test)
156+
assert KV.Bucket.get(config.test, "milk") == nil
157+
158+
KV.Bucket.put(config.test, "milk", 3)
159+
assert KV.Bucket.get(config.test, "milk") == 3
160+
end
168161
```
169162

170-
You can read more about ExUnit cases in the [`ExUnit.Case` module documentation](`ExUnit.Case`) and more about callbacks in `ExUnit.Callbacks`.
163+
The `config` argument, passed after the test name, is the *test context* and it includes configuration and metadata about the current test, which is useful in scenarios like these.
171164

172165
## Other agent actions
173166

@@ -214,4 +207,4 @@ end
214207

215208
When a long action is performed on the server, all other requests to that particular server will wait until the action is done, which may cause some clients to timeout.
216209

217-
In the next chapter, we will explore GenServers, where the segregation between clients and servers is made more apparent.
210+
Some APIs, such as GenServers, make a clearer distiction between client and server, and we will explore them in future chapters. Next let's talk about naming things, applications, and supervisors.

0 commit comments

Comments
 (0)