You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: lib/elixir/pages/mix-and-otp/agents.md
+31-38Lines changed: 31 additions & 38 deletions
Original file line number
Diff line number
Diff line change
@@ -3,27 +3,22 @@
3
3
SPDX-FileCopyrightText: 2021 The Elixir Team
4
4
-->
5
5
6
-
# Simple state management with agents
6
+
# Simple state with agents
7
7
8
8
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.
9
9
10
10
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.
11
11
12
12
## The trouble with (mutable) state
13
13
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.
15
15
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:
20
17
21
18
*`Agent` — Simple wrappers around state.
22
19
*`GenServer` — "Generic servers" (processes) that encapsulate state, provide sync and async calls, support code reloading, and more.
23
20
*`Task` — Asynchronous units of computation that allow spawning a process and potentially retrieving its result at a later time.
24
21
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
-
27
22
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.
28
23
29
24
## Agents 101
@@ -47,7 +42,7 @@ iex> Agent.stop(agent)
47
42
:ok
48
43
```
49
44
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.
51
46
52
47
The `Agent.update/3` function accepts as a second argument any function that receives one argument and returns a value:
53
48
@@ -93,7 +88,9 @@ Also note the `async: true` option passed to `ExUnit.Case`. This option makes th
93
88
Async or not, our new test should obviously fail, as none of the functionality is implemented in the module being tested:
94
89
95
90
```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)
97
94
```
98
95
99
96
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
104
101
105
102
@doc """
106
103
Starts a new bucket.
104
+
105
+
All options are forwarded to `Agent.start_link/2`.
107
106
"""
108
-
defstart_link(_opts) do
109
-
Agent.start_link(fn-> %{} end)
107
+
defstart_link(opts) do
108
+
Agent.start_link(fn-> %{} end, opts)
110
109
end
111
110
112
111
@doc """
@@ -125,49 +124,43 @@ defmodule KV.Bucket do
125
124
end
126
125
```
127
126
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.
129
128
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.
131
130
132
131
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.
133
132
134
133
Now that the `KV.Bucket` module has been defined, our test should pass! You can try it yourself by running: `mix test`.
135
134
136
-
## Test setup with ExUnit callbacks
135
+
## Naming processes
137
136
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.
139
138
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:
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:
163
152
164
153
```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
168
161
```
169
162
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.
171
164
172
165
## Other agent actions
173
166
@@ -214,4 +207,4 @@ end
214
207
215
208
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.
216
209
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