Словарь

Словарь — способ хранения состояния актора. То же: словарь процесса, словарь актора, process dictionary.

Словарь иногда бывает полезен, а именно когда мы очень торопимся или когда ещё не продумали должным образом архитектуру кода. То есть когда нет возможности хорошо подумать и сделать наш код в функциональном ключе, то есть без побочных эффектов.

Обычная независимая функция всегда работает так: аргументы -> обработка этих аргументов -> результат. При одних и тех же аргументах нормальная функция всегда должна возвращать один и тот же результат. Бывают, конечно, и зависимые функции, которые при одних и тех же аргументах могут возвращать разные значения. Такова, например, функция spawn, каждый раз возвращающая новый пид. Но про любую качественную функцию можно сказать, что она обладает чёткой логикой, не закладывает опасных “мин”.

Если использовать словарь, то можно очень легко запутаться. Во-первых, кляузы функции получают доступ не только к аргументам (явным входным данным), но и к словарю (неявным входным данным). Во-вторых, кляузы могут изменить состояние словаря, и таким образом сформируют неявный результат.

Пример, показывающий как опасно использовать словарь

-module(proba).
-export([main/1]).

main(Input) ->
    put(input, Input), % СЛОВАРЬ!
    some().

some() ->
    io:format("I got it: ~p ~n", [get(input)]). % СЛОВАРЬ!

В теле функции main/1, которая получает некую входную информацию извне, содержится функция put/2. В данном случае для ключа-атома input задаётся значение Input. Мы запускаем функцию some/0, и она с помощью get(input) читает значение словаря для ключа input и выводит это на печать.

Если put/2 в нашем коде визуально хорошо заметно, то get/1 уже спряталось. Если мы через год вернёмся к своему коду, можем эту его тонкую особенность уже не приметить.

Что мы получаем в результате? А в результате мы получаем, что функция some/0, которая вообще не принимает никаких аргументов, тем не менее в разных ситуациях ведёт себя по-разному.

1> proba:main("The Beatles").
I got it: "The Beatles" 
ok
2> proba:main("The Doors").  
I got it: "The Doors" 
ok

Подправим немного наш код, чтобы убедиться, что some/0 может не только читать, но и менять глобальное состояние актора.

-module(proba).
-export([main/1]).

main(Input) ->
    put(input, Input),
    some(),
    io:format("Checking: ~p ~n", [get(input)]).

some() ->
    io:format("I got it: ~p ~n", [get(input)]),
    put(input, re:replace(get(input), "ea", "ot", [{return,list},global])).

Здесь функция some/0 “незаметно” в полученном тексте меняет две буквы “ea” на две буквы “ot” и сохраняет получившееся в словаре.

1> proba:main("The Beatles"). 
I got it: "The Beatles" 
Checking: "The Bottles" 
ok
6> get().
[{input,"The Bottles"}]

Функция main/1 проверяет после вызова some/0 состояние словаря и с удивлением обнаруживает, что он изменился.

Кстати, обратите внимание, что оболочке тоже можно посмотреть состояние словаря, вызвав get().

put/2 записывает в словарь новое значение, а старое — возвращает. Если значения для данного ключа ещё нет, будет возвращено undefined.

1> put(ded, moroz). 
undefined
2> put(ded, santa). 
moroz

Тут, кстати, тоже возможна путаница. В нашем примере выше функция some/0 тоже что-то возвращает, и это что-то значение выражения put(…). В будущем, когда мы будем пересматривать наш код, нам может показаться, что он работает нормально, раз some/0 возвращает “The Beatles”. При этом мы можем не обратить внимания на то, что в словаре теперь у нас записано “The Bottles”.

Словарь и акторы

Словарь в оригинале называется process dictionary, то есть словарь процесса. Этим самым хотят подчеркнуть, что у каждого процесса (то есть актора) свой словарь, то есть с помощью put() или get() можно запоминать и воспроизводить состояние актора.

Словарь, понятно, может хранить любые термы Эрланга. Поэтому он, очевидно, и называется словарём, ибо в нём хранятся (записываются) terms (термины).

Попробуем проверить, правда ли, что у каждого актора свой словарь.

-module(proba).
-export([main/0,animal/1]).

main() ->
    put(name, "Gorilla"),
    spawn(proba, animal, ["Zebra"]),
    spawn(proba, animal, ["Elephant"]),
    spawn(proba, animal, ["Lion"]),
    timer:sleep(100),
    io:format("Checking: ~p ~n", [get()]).

animal(Name) -> 
    Some = put(name, Name),
    io:format("(~p) Some: ~p ~n", [Name, Some]),
    io:format("(~p) All: ~p ~n", [Name, get()]).

Функцию animal/1 мы будем спаунить как актора. Всё, что этот актор будет делать: принимать своё имя аргументом Name, записывать своё имя в словарь, выводить результат этой операции, а потом выводить весь словарь.

main/0 начинает свою работу с того, что записывает в словарь своё имя (“Gorilla”). Потом она спаунит трёх акторов, давая сразу каждому его уникальное имя. После этого она засыпает на 100 мс, чтобы дать возможность акторам наиграться со словарём, и в конце распечатывает весь свой словарь.

1> proba:main().
("Zebra") Some: undefined 
("Elephant") Some: undefined 
("Lion") Some: undefined 
("Zebra") All: [{name,"Zebra"}] 
("Elephant") All: [{name,"Elephant"}] 
("Lion") All: [{name,"Lion"}] 
Checking: [{name,"Gorilla"}] 
ok

Всё как и ожидалось. У каждого из четырёх акторов (включая главный) свой словарь.

Операции со словарём

Вот все бифы, предназначенные для работы со словарём:

Документация

Processes — глава про акторы, в т.ч. и про словарь.


© Алексей Карманов, 2024.