Let it crash или нет?

145 views
Skip to first unread message

Aklaim

unread,
Jul 22, 2016, 4:02:00 AM7/22/16
to Erlang по-русски
Всем привет.

Пишу небольшой проект на erlang и попутно его осваиваю.
Но не хватает опыта написания сложной логики на erlang.
Почти все функции что я пишу предпологают защитное программирование и возвращаяют {ok, Data} или {error, Reason}, так как работают с внешними источниками данных, либо в ходе обработки могут быть проблемы. В ситуации когда необходимо получить задание откуда то из вне, есть шанс, что задания нет. В таких случая возвращаю {error, Reason} и просто пытаюсь снова получить задание через какое то время.
С одной стороны мне это нравится тем, что даже если что то пошло не так, всегда есть возможность обработать ошибочную ситуацию и не дать упасть процессу ради чистого перезапуска.

С другой стороны на каждом шагу появляются конструкции вида:
case some_fun() of
     {ok, Data} ->
          nex_fun(Data);

        {error, Reason} ->
             log_error(Reason),
             {error, Reason}
end.


При чем чем сложнее логика тем больше вложенность case, так как на каждом шаге могут быть проблемы. Все вложенные части я стараюсь разбевать на отдельные функции, чтобы код был чище. Но эта вложенность очень напрягает. Не покидает ощущение, что получается криво. Ну и внести изменения потом мягко говоря проблематично.

Поэтому думаю над следующим стилем написания кода:

process() ->
 try handle_process() of
   {ok, Data} ->
     Data
 catch
   throw:Error ->
     log_error(Error)
 end.

handle_process() ->
 Task = get_task(),

  process_task(Task).

get_task() ->
 case get_task_from_somewhere() of
   {ok, Task} ->
     Task;

    {error, Reason} ->
     throw({error, get_task, Reason})
 end.

process_task(Task) ->
 Result = some_process_fun(Task),
 some_other_process_fun(Result).

some_process_fun(Task) ->
 %% Что то пошло не так ...
 throw({error, something_bad_happened}).

В таком случае вложенность будет по миниму и функции будут выполнять только свою роль, не придется тащить в каждую весь state, так как он может дальше пригодится. Но при таком подходе в коде будет куча throw и это по моему тоже не дело. Получается throw станет своего рода return.

Посмотрел большие erlang проекты mochiweb, cowboy, rabbitmq-server. В некоторых в обще 2-3 throw на весь проект. 
Так что сомнения по поводу нового подхода имеются. Поэтому нужен совет.

Aleksey Kluchnikov

unread,
Jul 22, 2016, 5:04:00 AM7/22/16
to erlang-russian
Весь стейт тащить в принципе ничего страшного, там же передается указатель на него а не данные.

При поступлении запроса делается стейт #{req => Req}
В стейте заводится поле #{status => ok} и каждая функция передает стейт следующей функции если все ок или возвращает #{status := {err, Reason}}.

Например как нибудь примерно так..

incoming_req(State, Req) ->
  ReqState = #{state => State, req => Req, status => ok, answer => []}
  {NewState, Answer} =
    case try manage_req(ReqState) catch E:R -> {err, {E,R}} end of
       #{status := ok, state := NewStateValue, answer := AnswerValue} -> {NewStateValue, AnswerValue};
       #{status := Err} -> {State, Err};
       Err -> {State, Err}
    end,
  {NewState, Answer}.

получится что то вроде транзакции для State. Конечно если в обработке запроса много нечистых функций то все усложняется.

22 июля 2016 г., 12:02 пользователь Aklaim <akl.d...@gmail.com> написал:

--
Вы получили это сообщение, поскольку подписаны на группу "Erlang по-русски".
Чтобы отменить подписку на эту группу и больше не получать от нее сообщения, отправьте письмо на электронный адрес erlang-russia...@googlegroups.com.
Чтобы отправлять сообщения в эту группу, отправьте письмо на электронный адрес erlang-...@googlegroups.com.
Чтобы настроить другие параметры, перейдите по ссылке https://groups.google.com/d/optout.

Aklaim

unread,
Jul 22, 2016, 5:37:12 AM7/22/16
to Erlang по-русски
На такого плана варианте пока остановился.
Но с ним тоже не все так гладко, все функции обработки задания получаются с примерно таким spec

-spec process_req(State :: #state{}) -> Result :: #state{}.

Сама обработка при этом примерно выглядит так:

process_req(#state{status = get_task}) ->
  %% ...
  get_task();

process_req(#state{status = process_task}) ->
  %% ...
  process_task();

process_req(#state{status = error}) ->
  %% ...
  {error, Reason}.

Пытаюсь использовать dialyzer по возможности, но такие функции он всегда будет помечать как валидные.

Спасибо за помощь!
Пока буду продолжать писать как получается, со временем может прийду к какому то приемлемому решению.

Aleksey Kluchnikov

unread,
Jul 22, 2016, 5:55:59 AM7/22/16
to erlang-russian
пробовал так, через некоторое время устал, оказалось тяжело работать с порядком функции через патерн матчинг переменной статуса. Как то все время надо выдумывать этот статус и смотеть что он нигде до этого не проматчится. Проще таки разные имена функций, их если надублируешь компилятор ругнется.

22 июля 2016 г., 13:37 пользователь Aklaim <akl.d...@gmail.com> написал:

--

Danil A. Zagoskin

unread,
Jul 22, 2016, 6:45:19 AM7/22/16
to Erlang по-русски
Я иногда для подобных задач использую что-то вроде (псевдокод, но надеюсь донести идею):

do_something(State) ->
  lists:foldl(fun apply_if_ok/2, {ok, State}, [fun transform1/1, fun transform2/1, fun transform3/1]).

apply_if_ok(Function, {ok, State}) -> Function(State);
apply_if_ok(_, {error, State}) -> {error, State}.

transform1(State) -> {ok, State}. % Will be called
transform2(State) -> {error, State}. % Will be called
transform3(State) -> {ok, State}. % Will not be called because transform2 fails

Тут логика «если уже есть ошибка, то дальше ничего не делать» сконцентрирована в специальной функции.
Минус такого подхода в том, что обычно для такой цепочки преобразований нужно городить тип-контейнер, который передавался бы как State, а также в том, что функции с преобразованиями должны иметь строго одинаковый интерфейс (под специальный тип стейта).
Удобство среди прочего в том, что порядок преобразований можно записать в столбик, что позволяет легко комментировать и менять местами шаги.

Yuri Zhloba

unread,
Jul 22, 2016, 6:53:23 AM7/22/16
to erlang-...@googlegroups.com
Да, это не очень удобная задача для новичков в ФП. Сразу сталкивает с монадами. Которых в эрланге и нет, так что приходится выкручиваться. Например, так: https://github.com/habibutsu/erlz

22 июля 2016 г., 13:44 пользователь Danil A. Zagoskin <da...@st-olen.ru> написал:



--
Yuri Zhloba

skype: yzh44yzh
phone: +375 44 793 33 73

Aklaim

unread,
Jul 22, 2016, 7:20:34 AM7/22/16
to Erlang по-русски, da...@st-olen.ru
По моему весьма неплохо.
Попробую такой вариант. Легче будет чем матчить какой то статус в State.  Ну и все шаги по порядку расписаны будут. Спасибо!

On Friday, July 22, 2016 at 1:45:19 PM UTC+3, Danil A. Zagoskin wrote:

Сергей Ивлев

unread,
Jul 22, 2016, 7:28:24 AM7/22/16
to erlang-...@googlegroups.com
можно сделать первый шаг к монадам https://github.com/rabbitmq/erlando
do([error_m ||
            SomeResult <- some_fun(),
            AnotherResult <- another_fun(SomeResult),
            LastResult <- last_fun(AnotherResult ),
            return(LastResult )
       ]).

22 июля 2016 г., 14:20 пользователь Aklaim <akl.d...@gmail.com> написал:
По моему весьма неплохо.
Попробую такой вариант. Легче будет чем матчить какой то статус в State.  Ну и все шаги по порядку расписаны будут. Спасибо!

On Friday, July 22, 2016 at 1:45:19 PM UTC+3, Danil A. Zagoskin wrote:

Aklaim

unread,
Jul 22, 2016, 7:53:41 AM7/22/16
to Erlang по-русски
Пока лезть в такие дебри не хочется.
Осваивать дополнительные инструменты ради, того чтобы код был чище, но при этом не особо понятным, это уже перебор. 
Такими темпами уже лучше на elixir писать с его pipe (|>) оператором.

Yuri Zhloba

unread,
Jul 22, 2016, 7:55:35 AM7/22/16
to erlang-...@googlegroups.com
"Я иногда для подобных задач использую что-то вроде (псевдокод, но надеюсь донести идею):"
"По моему весьма неплохо.Попробую такой вариант."

Дык это и есть монада. Просто нет поддержки со стороны синтаксиса языка, и поэтому приходится функции складывать в список.

22 июля 2016 г., 14:53 пользователь Aklaim <akl.d...@gmail.com> написал:

Yuri Zhloba

unread,
Jul 22, 2016, 7:57:20 AM7/22/16
to erlang-...@googlegroups.com
А ссылки выше, это такое же решение, просто обобщенное для большего числа случаев.

Но, конечно, эти либы есть смысл брать, если хорошо понимаешь, что там происходит. А хорошее понимание можно взять в книжках по Хаскелю. В книжках по Эрлангу про такое не пишут.

22 июля 2016 г., 14:55 пользователь Yuri Zhloba <yzh4...@gmail.com> написал:

Aklaim

unread,
Jul 22, 2016, 8:09:31 AM7/22/16
to Erlang по-русски
Да у книжек по erlang туго с объяснениями функционального программирования. 
Ну не все сразу, если что то будет получатся уже подтяну мат часть.

Dmitry Dmitriy

unread,
Jul 24, 2016, 2:18:33 AM7/24/16
to Erlang по-русски
Какая матчасть?! Ху*чь код блеать!)
Шучу, каэш!
А вообще не косяк ли это в дизайне, что ваш сервис сам опрашивает какое-то хранилище периодически? Не проще ли ему посылать таски сообщением, а он пусть спит до получения следующей задачи. Обычный ген сервер. И сразу уже никакого негатив кейса не надо. И тут уже обработка исключений будет нужна если задача реально свалилась, при парсинге аргументов например. И тут уж решайте let it crash - если данная ошибка подразумевает, что поддерево, или сервис целиком перешло в какое то некорректное состояние; или обработка исключения - если это просто пользователь ввел некорректные данные и они прилетели из сети. Падать постоянно не нужно, как и пытаться выжить во что бы то ни стало.
По поводу возвращения ошибок. Мое мнение такое:
1). бросать исключения самое оно, когда сервис получил неверные данные.
2). бросать исключения в принципе нормально, когда данные верные, но не в этом состоянии сервиса - не проинициализирован, или повторно добавляется таска.
3). бросать исключения так же можно в негатив кейсах которые напрямую не зависят от состяния именно этой таски - таймаут посылки запроса, нет прав для чтения файла. Если эти ошибки прокидывать наверх через возвращение переменной, то код будет труднее читать.
4). остальные ошибки, связанные именно с логикой таски стоит возвращать обычным образом. Может конечно из-за этого "элегантное" решение из двух строк превратится в 4 или (о боже!) 16 строк, зато тот кто будет разбираться в вашем не будет вас проклинать.
5). когда пишите код помните что пишите его для того чтобы его читали и понимали люди. Легко читали и быстро понимали.
6). используйте комментарии
7). мир вам

пятница, 22 июля 2016 г., 18:09:31 UTC+6 пользователь Aklaim написал:
Reply all
Reply to author
Forward
0 new messages