Elixir: Состояние процесса
В прошлом упражнении мы создали процесс-калькулятор, который только обрабатывает входящие запросы, не храня никакого состояния. Но как его сохранить? Основная идея процесса, который обрабатывает более одного сообщения за раз, это вызов рекурсивной функции. Повторный вызов которой, продолжит основной цикл обработки сообщений. Рассмотрим пример с процессом-счетчиком:
defmodule Counter do
def init(parent, initial_state \\ 0) do
process(parent, initial_state)
end
defp process(parent, state) do
receive do
:inc ->
new_state = state + 1
send(parent, {:ok, new_state})
process(parent, new_state)
:dec ->
new_state = state - 1
send(parent, {:ok, new_state})
process(parent, new_state)
:current ->
send(parent, {:ok, state})
process(parent, state)
:reset ->
send(parent, {:ok, 0})
process(parent, 0)
end
end
endВ процессе-счетчике мы вынесли функцию запуска процесса, запомнили процесс-родитель и инициализировали состояние процесса-счетчика нулем. Затем при обработке сообщения изменяется состояние процесса, которое передается дальше в рекурсивный вызов функции process. Теперь запустим счетчик и проверим его работу:
Важно, что идентификатор родительского процесса мы передаем снаружи, так как вызов self() внутри модуля вернет иной идентификатор.
parent = self()
counter = spawn(fn -> Counter.init(parent) end)
send(counter, :current)
# для просмотра почтового ящика родительского процесса
# далее по коду будет подразумеваться, что эта строчка кода
# будет вызываться после каждой отправки сообщения другому процессу
Process.info(parent, :messages)
# => {:messages, [ok: 0]}
send(counter, :inc)
# => {:ok, 1}
send(counter, :inc)
# => {:ok, 2}
send(counter, :dec)
# => {:ok, 1}
send(counter, :inc)
# => {:ok, 2}
send(counter, :reset)
# => {:ok, 0}
send(counter, :dec)
# => {:ok, -1}Важно заметить, что каждый процесс хранит свое состояние отдельно от других процессов:
parent = self()
first_counter = spawn(fn -> Counter.init(parent) end)
second_counter = spawn(fn -> Counter.init(parent) end)
send(first_counter, :inc)
send(first_counter, :inc)
send(first_counter, :inc)
send(second_counter, :dec)
send(second_counter, :dec)
send(first_counter, :current)
# => {:ok, 3}
send(second_counter, :current)
# => {:ok, -2}Состояние не обязательно выражается числом, в него можно складывать что-угодно, начиная от чисел и заканчивая словарем со ссылками на другие процессы, к примеру.
Задание
В этот раз создайте процесс кеш-сервер, который хранит в состоянии словарь. Напишите функцию init, которая принимает родительский процесс и начальное состояние (как в примерах выше). Затем добавьте обработку следующих сообщений:
:putположить по переданному ключуkeyзначениеvalueв состояние кеш-сервера, возвращает кортеж{:ok, value};:getдостать из состояния процесса значение по переданному ключу, при отсутствии значения вернуть кортеж{:error, :not_found};:dropубрать из состояния процесса переданный ключ;:exitперестать обрабатывать входящие сообщения кеш-сервером, возвращает кортеж{:ok, :exited};- а еще добавьте обработку невалидных сообщений, чтобы процесс не ломался, если ему передать что-то не то (вернуть кортеж
{:error, :unrecognized_operation}).
parent = self()
cache_server = spawn(fn -> CacheServer.init(parent) end)
send(cache_server, {:put, {:hello, "world"}})
# => {:ok, "world"}
send(cache_server, {:get, {:hello}})
# => {:ok, "world"}
send(cache_server, {:get, {:some}})
# => {:error, :not_found})
send(cache_server, {:drop, {:hello}})
# => {:ok, :hello}
send(cache_server, {:get, {:hello}})
# => {:error, :not_found})
send(cache_server, {:something, {1, 2, 3}})
# => {:error, :unrecognized_operation}
Process.alive?(cache_server)
# => true
send(cache_server, :boom)
# => {:error, :unrecognized_operation}
Process.alive?(cache_server)
# => true
send(cache_server, {:exit})
# => {:ok, :exited}
Process.alive?(cache_server)
# => falseПолезное
Elixir: Состояние процесса
В прошлом упражнении мы создали процесс-калькулятор, который только обрабатывает входящие запросы, не храня никакого состояния. Но как его сохранить? Основная идея процесса, который обрабатывает более одного сообщения за раз, это вызов рекурсивной функции. Повторный вызов которой, продолжит основной цикл обработки сообщений. Рассмотрим пример с процессом-счетчиком:
defmodule Counter do
def init(parent, initial_state \\ 0) do
process(parent, initial_state)
end
defp process(parent, state) do
receive do
:inc ->
new_state = state + 1
send(parent, {:ok, new_state})
process(parent, new_state)
:dec ->
new_state = state - 1
send(parent, {:ok, new_state})
process(parent, new_state)
:current ->
send(parent, {:ok, state})
process(parent, state)
:reset ->
send(parent, {:ok, 0})
process(parent, 0)
end
end
endВ процессе-счетчике мы вынесли функцию запуска процесса, запомнили процесс-родитель и инициализировали состояние процесса-счетчика нулем. Затем при обработке сообщения изменяется состояние процесса, которое передается дальше в рекурсивный вызов функции process. Теперь запустим счетчик и проверим его работу:
Важно, что идентификатор родительского процесса мы передаем снаружи, так как вызов self() внутри модуля вернет иной идентификатор.
parent = self()
counter = spawn(fn -> Counter.init(parent) end)
send(counter, :current)
# для просмотра почтового ящика родительского процесса
# далее по коду будет подразумеваться, что эта строчка кода
# будет вызываться после каждой отправки сообщения другому процессу
Process.info(parent, :messages)
# => {:messages, [ok: 0]}
send(counter, :inc)
# => {:ok, 1}
send(counter, :inc)
# => {:ok, 2}
send(counter, :dec)
# => {:ok, 1}
send(counter, :inc)
# => {:ok, 2}
send(counter, :reset)
# => {:ok, 0}
send(counter, :dec)
# => {:ok, -1}Важно заметить, что каждый процесс хранит свое состояние отдельно от других процессов:
parent = self()
first_counter = spawn(fn -> Counter.init(parent) end)
second_counter = spawn(fn -> Counter.init(parent) end)
send(first_counter, :inc)
send(first_counter, :inc)
send(first_counter, :inc)
send(second_counter, :dec)
send(second_counter, :dec)
send(first_counter, :current)
# => {:ok, 3}
send(second_counter, :current)
# => {:ok, -2}Состояние не обязательно выражается числом, в него можно складывать что-угодно, начиная от чисел и заканчивая словарем со ссылками на другие процессы, к примеру.
Задание
В этот раз создайте процесс кеш-сервер, который хранит в состоянии словарь. Напишите функцию init, которая принимает родительский процесс и начальное состояние (как в примерах выше). Затем добавьте обработку следующих сообщений:
:putположить по переданному ключуkeyзначениеvalueв состояние кеш-сервера, возвращает кортеж{:ok, value};:getдостать из состояния процесса значение по переданному ключу, при отсутствии значения вернуть кортеж{:error, :not_found};:dropубрать из состояния процесса переданный ключ;:exitперестать обрабатывать входящие сообщения кеш-сервером, возвращает кортеж{:ok, :exited};- а еще добавьте обработку невалидных сообщений, чтобы процесс не ломался, если ему передать что-то не то (вернуть кортеж
{:error, :unrecognized_operation}).
parent = self()
cache_server = spawn(fn -> CacheServer.init(parent) end)
send(cache_server, {:put, {:hello, "world"}})
# => {:ok, "world"}
send(cache_server, {:get, {:hello}})
# => {:ok, "world"}
send(cache_server, {:get, {:some}})
# => {:error, :not_found})
send(cache_server, {:drop, {:hello}})
# => {:ok, :hello}
send(cache_server, {:get, {:hello}})
# => {:error, :not_found})
send(cache_server, {:something, {1, 2, 3}})
# => {:error, :unrecognized_operation}
Process.alive?(cache_server)
# => true
send(cache_server, :boom)
# => {:error, :unrecognized_operation}
Process.alive?(cache_server)
# => true
send(cache_server, {:exit})
# => {:ok, :exited}
Process.alive?(cache_server)
# => falseПолезное
Ваше упражнение проверяется по этим тестам
defmodule Test do
use ExUnit.Case
describe "cache server work" do
test "put" do
parent = self()
cache_server = spawn(fn -> CacheServer.init(parent) end)
send(cache_server, {:put, {:hello, "world"}})
assert_receive({:ok, "world"})
assert Process.alive?(cache_server)
send(cache_server, {:put, {:key, "value"}})
assert_receive({:ok, "value"})
assert Process.alive?(cache_server)
end
test "get" do
parent = self()
cache_server = spawn(fn -> CacheServer.init(parent) end)
send(cache_server, {:put, {:hello, "world"}})
send(cache_server, {:get, {:hello}})
assert_receive({:ok, "world"})
assert Process.alive?(cache_server)
send(cache_server, {:get, {:some}})
assert_receive({:error, :not_found})
assert Process.alive?(cache_server)
end
test "drop" do
parent = self()
cache_server = spawn(fn -> CacheServer.init(parent) end)
send(cache_server, {:put, {:hello, "world"}})
send(cache_server, {:drop, {:hello}})
assert_receive({:ok, :hello})
assert Process.alive?(cache_server)
send(cache_server, {:drop, {:some}})
assert_receive({:ok, :some})
assert Process.alive?(cache_server)
end
test "exit" do
parent = self()
cache_server = spawn(fn -> CacheServer.init(parent) end)
send(cache_server, {:exit})
assert_receive({:ok, :exited})
refute Process.alive?(cache_server)
end
test "unexpected input" do
parent = self()
cache_server = spawn(fn -> CacheServer.init(parent) end)
send(cache_server, {:something, {1, 2, 3}})
assert_receive({:error, :unrecognized_operation})
assert Process.alive?(cache_server)
send(cache_server, :boom)
assert_receive({:error, :unrecognized_operation})
assert Process.alive?(cache_server)
end
end
endРешение учителя откроется через:
20:00
