Комментарии 12
Классный разбор внутренностей Elixir'a, спасибо!
А вы не могли бы подробнее описать, почему решили остановиться на Erlang'е? Явно ведь не из-за протоколов — статья подводит к тому, что они удобны, а оверхед у них небольшой.
1. Erlang в нашей компании стал использоваться еще до появления Elixir. Соответственно у нас есть множество библиотек и устоявшихся практик как писать код на Erlang быстро и красиво. Взаимодействие Elixir -> Erlang писать достаточно легко, а вот Erlang -> Elixir значительно сложнее. К тому же сам по себе код на языке Erlang, хоть и выглядит поначалу непривычно, из-за своей простоты (малого количества синтаксических конструкций) довольно легко как читать, так и писать (особенно, после того как добавились мапы).
А конструкции как Protocols или pipe оператор вполне можно «повторить».
2. Erlang у нас используется не только для Web API но и для написания внутренних БД (например есть проект riak-core) и очень часто. Elixir же развивается в основном вокруг framework-a Phoenix и в таких проектах не дает ощутимых преимуществ.
3. Erlang стабильнее Elixir и «детские болезни» прошел уже давно. А для нас важна стабильность.
Как замена pipe чаще всего используется каррирование, замыкания и свертки.
...
Output = pipe(Input, [
fetch_users(),
update_users(),
store_users_in_database(DbConnection)
]),
...
где pipe/2
— простая свертка, например.
pipe(Data, Funs) ->
lists:foldl(fun(F, D) -> F(D) end, Data, Funs).
плюс в том что на таких pipe-ах можно построить нечто похожее на монады.
pipe(_Bind, Data, []) ->
Data;
pipe(Bind, Data, [H|T]) ->
Bind(Data, fun(D) -> pipe(Bind, H(D), T) end).
maybe(F, {just, Data}) -> F(Data);
maybe(F, nothing) -> nothing.
Usage
1> m:pipe(fun m:maybe/2, {just, 11}, [
fun(A) ->
case A > 10 of
true -> {just, A};
false -> nothing
end
end,
fun(A) -> {just, A*2} end
]).
{just, 22}.
или, если причесать через каррирование,
filter_gt(A) -> fun(B) ->
case A > B of
true -> {just, A}:
false -> nothing
end
end.
do_mult(A) -> fun(B) -> {just, A*B} end.
то будет просто
1> m:pipe(fun m:maybe/2, {just, 11}, [
m:filter_gt(10),
m:mult(2)
]).
{just, 22}
но чаще используется pipe не на столько абстрактный, а под конкретный случай, например ok/error.
pipe(D, []) -> D;
pipe({ok, D}, [H|T]) -> pipe(H(D), T);
pipe({error, _}=Err, _) -> Err.
Конечно не сравнится с do нотацией Haskell или for в Scala, но жить можно :)
Очень хорошо такой подход себя показал в нашей библиотеке для работы с Postgres
1> repo:all(m_weather, [
q:where(fun([#{city := City}]) -> pg_sql:in(City, [<<"Kraków">>, <<"Moscow">>]) end),
q:order_by(fun([#{temp_lo := T}]) -> [{T, asc}] end),
q:limit(10)
]).
Но также у него есть один минус Dialyzer не сможет провести детальную проверку типов, если State, который передается сквозь функции, меняет тип.
К слову, я экспериментировал с добавлением pipe оператора в нативный синтаксис Erlang и это оказалось проще чем я думал :)
Действительно, "декомпилировать beam-файлы и посмотреть, что же в итоге получилось", зачастую является лучшим способом понять, что же делает компилятор Эликсира.
У такого подхода есть один несущественный минус – вы не можете определить несколько реализаций протокола для одного и того же типа
А вам нужна такая возможность?
Как устроены протоколы в Elixir