- PVSM.RU - https://www.pvsm.ru -
На Хабре можно найти много публикаций, раскрывающих как теорию монад, так и практику их применения. Большинство этих статей ожидаемо про Haskell. Я не буду в n-й раз пересказывать теорию. Сегодня мы поговорим про некоторые проблемы Erlang, способы их решения с помощью монад, частичного применения функций и синтаксического сахара из erlando [1] – классной библиотеки от команды RabbitMQ.
В Erlang есть иммутабельность, а монад нет*. Но благодаря наличию в языке функционала parse_transform и реализации erlando, возможность использования монад в Erlang все же есть.
Про иммутабельность в самом начале повествования, я заговорил не случайно. Иммутабельность почти везде и всегда – одна из основных идей Erlang. Иммутабельность и чистота функций позволяет концентрировать свое внимание на разработке конкретной функции и не бояться сайд эффектов. Но новичкам в Erlang, пришедшим, например, из Java или Python, довольно трудно понять и принять идеи Erlang. Особенно если вспомнить про синтаксис Erlang. Кто пытался начать использовать Erlang, наверняка отмечал его необычность и самостийность. Во всяком случае, у меня накопилось много отзывов новичков и “странный” синтаксис лидирует в рейтинге.
Erlando – набор расширений Erlang, дающий нам:
Замечание: Нижеприведенные примеры кода для иллюстрации фич erlando я взял из выступления Matthew Sackman’a, частично разбавив их своим кодом и объяснениями.
Сразу к делу. Рассмотрим несколько функций из реального проекта:
info_all(VHostPath, Items) ->
map(VHostPath, fun (Q) -> info(Q, Items) end).
backing_queue_timeout(State = #q{ backing_queue = BQ }) ->
run_backing_queue(
BQ, fun (M, BQS) -> M:timeout(BQS) end, State).
reset_msg_expiry_fun(TTL) ->
fun (MsgProps) ->
MsgProps #message_properties{
expiry = calculate_msg_expiry(TTL)}
end.
Все эти функции созданы для подстановки параметров в простые выражения. На самом деле это частичное применение, так как некоторые параметры не будут известны до вызова. Вместе с гибкостью, эти функции привносят шум в наш код. Изменив немного синтаксис – введя cut – можно улучшить ситуацию.
Cut использует _ в выражениях для указания, где должна быть применена абстракция. Cut оборачивает только ближайший уровень в выражении, но применение вложенных cut не запрещено.
Например list_to_binary([1, 2, math:pow(2, _)]).
развернется в list_to_binary([1, 2, fun (X) -> math:pow(2, X) end]).
но не в fun (X) -> list_to_binary([1, 2, math:pow(2, X)]) end.
.
Звучит слегка непонятно, давайте перепишем примеры выше с использованием cut:
info_all(VHostPath, Items) ->
map(VHostPath, fun (Q) -> info(Q, Items) end).
info_all(VHostPath, Items) -> map(VHostPath, info(_, Items)).
backing_queue_timeout(State = #q{ backing_queue = BQ }) ->
run_backing_queue(
BQ, fun (M, BQS) -> M:timeout(BQS) end, State).
backing_queue_timeout(State = #q{backing_queue = BQ}) ->
run_backing_queue(BQ, _:timeout(_), State).
reset_msg_expiry_fun(TTL) ->
fun (MsgProps) ->
MsgProps #message_properties {
expiry = calculate_msg_expiry(TTL) }
end.
reset_msg_expiry_fun(TTL) ->
_ #message_properties { expiry = calculate_msg_expiry(TTL) }.
Для иллюстрации порядка вычисления аргументов рассмотрим следующий пример:
f1(_, _) -> io:format("in f1~n").
test() ->
F = f1(io:format("test line 1~n"), _),
F(io:format("test line 2~n")).
Так как аргументы вычисляются до cut функции, на экран будет выведено:
test line 2
test line 1
in f1
F = {_, 3},
{a, 3} = F(a).
dbl_cons(List) -> [_, _ | List].
test() ->
F = dbl_cons([33]),
[7, 8, 33] = F(7, 8).
-record(vector, { x, y, z }).
test() ->
GetZ = _#vector.z,
7 = GetZ(#vector { z = 7 }),
SetX = _#vector{x = _},
V = #vector{ x = 5, y = 4 } = SetX(#vector{ y = 4 }, 5).
F = case _ of
N when is_integer(N) -> N + N;
N -> N
end,
10 = F(5),
ok = F(ok).
test() ->
GetZ = maps:get(z, _),
7 = GetZ(#{ z => 7 }),
SetX = _#{x => _},
V = #{ x := 5, y := 4 } = SetX(#{ y => 4 }, 5).
test_cut_comprehensions() ->
F = << <<(1 + (X*2))>> || _ <- _, X <- _ >>, %% Note, this'll only be a /2 !
<<"AAA">> = F([a,b,c], [32]),
F1 = [ {X, Y, Z} || X <- _, Y <- _, Z <- _,
math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2) ],
[{3,4,5}, {4,3,5}, {6,8,10}, {8,6,10}] =
lists:usort(F1(lists:seq(1,10), lists:seq(1,10), lists:seq(1,10))).
Pros
Cons
Программная запятая – конструкция связывания вычислений. Erlang не имеет ленивой модели вычислений. Давайте представим, что было бы, если Erlang был бы ленив как Haskell
my_function() ->
A = foo(),
B = bar(A, dog),
ok.
Чтобы гарантировать порядок выполнения, нам необходимо было бы явно связать вычисления, определив запятую.
my_function() ->
A = foo(),
comma(),
B = bar(A, dog),
comma(),
ok.
Продолжим преобразование:
my_function() ->
comma(foo(),
fun (A) -> comma(bar(A, dog),
fun (B) -> ok end)).
Исходя из вывода, comma/2 является идиоматической функцией >>=/2
. Монада требует только три функции: >>=/2
, return/1
и fail/1
.
Все бы ничего, но синтаксис просто ужасен. Применим трансформеры синтаксиса из erlando
.
do([Monad ||
A <- foo(),
B <- bar(A, dog),
ok]).
Поскольку do-блок параметризован, мы можем использовать монады различного типа. Внутри do-блока вызовы return/1
и fail/1
разворачиваются в Monad:return/1
и Monad:fail/1
соответственно.
Identity-monad.
Тождественная монада – простейшая монада, не меняющая тип значений и не участвующая в управлении процессом вычислений. Применяется с трансформерами. Выполняет связывание выражений – программная запятая, рассмотренная выше.
Maybe-monad.
Монада вычислений с обработкой отсутствующих значений. Связывание параметра с параметризованным вычислением – это передача параметра вычислению, связывание отсутствующего параметра с параметризованным вычислением – отсутствующий результат.
Рассмотрим пример применения maybe_m:
if_safe_div_zero(X, Y, Fun) ->
do([maybe_m ||
Result <- case Y == 0 of
true -> fail("Cannot divide by zero");
false -> return(X / Y)
end,
return(Fun(Result))]).
Вычисление выражения прекращается, если возвращается nothing.
{just, 6} = if_safe_div_zero(10, 5, _+4) ## 10/5 = 2 -> 2+4 -> 6
nothing = if_safe_div_zero(10, 0, _+4)
Error-monad.
Аналогично maybe_m, только с обработкой ошибок. Иногда принцип let it crash неприменим и ошибки нужно обработать в момент их возникновения. В этом случае в коде часто появляются лесенки из case, например такие:
write_file(Path, Data, Modes) ->
Modes1 = [binary, write | (Modes -- [binary, write])],
case make_binary(Data) of
Bin when is_binary(Bin) ->
case file:open(Path, Modes1) of
{ok, Hdl} ->
case file:write(Hdl, Bin) of
ok ->
case file:sync(Hdl) of
ok ->
file:close(Hdl);
{error, _} = E ->
file:close(Hdl),
E
end;
{error, _} = E ->
file:close(Hdl),
E
end;
{error, _} = E -> E
end;
{error, _} = E -> E
end.
make_binary(Bin) when is_binary(Bin) ->
Bin;
make_binary(List) ->
try
iolist_to_binary(List)
catch error:Reason ->
{error, Reason}
end.
Читать такое неприятно, выглядит как лапша callback в JS. На помощь приходит error_m:
write_file(Path, Data, Modes) ->
Modes1 = [binary, write | (Modes -- [binary, write])],
do([error_m ||
Bin <- make_binary(Data),
Hdl <- file:open(Path, Modes1),
Result <- return(do([error_m ||
file:write(Hdl, Bin),
file:sync(Hdl)])),
file:close(Hdl),
Result]).
make_binary(Bin) when is_binary(Bin) ->
error_m:return(Bin);
make_binary(List) ->
try
error_m:return(iolist_to_binary(List))
catch error:Reason ->
error_m:fail(Reason)
end.
P = [{X, Y, Z} || Z <- lists:seq(1,20),
X <- lists:seq(1,Z),
Y <- lists:seq(X,Z),
math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)].
То же самое только с list_m:
P = do([list_m || Z <- lists:seq(1,20),
X <- lists:seq(1,Z),
Y <- lists:seq(X,Z),
monad_plus:guard(list_m, math:pow(X,2) + math:pow(Y,2) == math:pow(Z,2)),
return({X,Y,Z})]).
State1 = init(Dimensions),
State2 = plant_seeds(SeedCount, State1),
{DidFlood, State3} = pour_on_water(WaterVolume, State2),
State4 = apply_sunlight(Time, State3),
{DidFlood2, State5} = pour_on_water(WaterVolume, State4),
{Crop, State6} = harvest(State5),
...
С помощью трансформатора и cut-нотации этот код можно переписать в более компактном и читаемом виде:
StateT = state_t:new(identity_m),
SM = StateT:modify(_),
SMR = StateT:modify_and_return(_),
StateT:exec(
do([StateT ||
StateT:put(init(Dimensions)),
SM(plant_seeds(SeedCount, _)),
DidFlood <- SMR(pour_on_water(WaterVolume, _)),
SM(apply_sunlight(Time, _)),
DidFlood2 <- SMR(pour_on_water(WaterVolume, _)),
Crop <- SMR(harvest(_)),
...
]), undefined).
Наверное, одна из моих любимых фич монады error_m
. Не важно, в каком месте произойдет ошибка, монада всегда вернет либо {ok, Result}
либо {error, Reason}
. Пример, иллюстрирующий поведение:
do([error_m ||
Hdl <- file:open(Path, Modes),
Data <- file:read(Hdl, BytesToRead),
file:write(Hdl, DataToWrite),
file:sync(Hdl),
file:close(Hdl),
file:rename(Path, Path2),
file:delete(Path),
return(Data)]).
На закуску у нас синтаксический сахар import_as. Стандартный синтаксис атрибута -import/2 позволяет импортировать в локальный модуль функции из других. Однако этот синтаксис не позволяет присвоить альтернативное название импортированной функции. Import_as решает эту проблему:
-import_as({my_mod, [{size/1, m_size}]})
-import_as({my_other_mod, [{size/1, o_size}]})
Эти выражения разворачиваются в настоящие локальные функции соответственно:
m_size(A) -> my_mod:size(A).
o_size(A) -> my_other_mod:size(A).
Конечно, монады позволяют контролировать процесс вычислений более выразительными методами, экономят код и время на его поддержку. С другой стороны, они привносят дополнительную сложность для неподготовленных членов команды.
* — на самом деле в Erlang монады существуют и без erlando. Запятая, разделяющая выражения – это конструкция линеаризации и связывания вычислений.
P.S. Недавно библиотека erlando была помечена авторами, как архивная. Данную статью я написал больше года назад. Тогда, впрочем, как и сейчас, на Хабре не было информации по монадам в Erlang. Чтобы исправить эту ситуацию, я публикую, хоть и с опозданием, данную статью.
Для использования erlando в erlang >= 22 необходимо исправить проблему с deprecated erlang:get_stacktrace/0. Пример фикса можно найти в моем форке: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2 [2]
Спасибо за ваше время!
Автор: Максим Молчанов
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/programmirovanie/329528
Ссылки в тексте:
[1] erlando: https://github.com/rabbitmq/erlando
[2] https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2: https://github.com/Vonmo/erlando/commit/52e23ecedd2b8c13707a11c7f0f14496b5a191c2
[3] Источник: https://habr.com/ru/post/466697/?utm_source=habrahabr&utm_medium=rss&utm_campaign=466697
Нажмите здесь для печати.