понедельник, 24 октября 2011 г.

Используем Erlang-макросы для сокращения повторений кода

Немного предисловия. Данную статью я хотел запостить на хабр, однако она не была отмодерирована и не попала даже в песочницу. Может я и правда написал что-то не нужное, не знаю, но поэтому пишу сюда.

В проекте, где я участвую, есть отдельный эрланг-модуль, у которого есть одна функция — request, следующего формата:
request(Cmd, Args, ClientInfo, MongoSettings) -> ...
Первым аргументом идёт binary(), который обозначает идентификатор метода (т.е. в этом модуле, назовём его api_methods, много-много определений функции request, с разными пре-заданными Cmd (паттерн матчинг, в общем)), второй — proplist с разными аргументами, ClientInfo — состояние клиента, MongoSettings — линк на монго-соединение, база, с которой мы работаем. В общем, это не так важно.

До сегодняшнего дня, определение метода выглядело примерно так:
request(<<"method1">>, Args, ClientInfo, MongoSettings) ->
  case {proplists:get_value(<<"arg1">>, Args, undefined),
        proplists:get_value(<<"arg2">>, Args, undefined)} of
    {Arg1, Arg2} when is_integer(Arg1) andalso
                      is_binary(Arg2) ->
      some_module:some_method(Arg1, Arg2, ClientInfo, MongoSettings);
    {_, _} ->
      {error_response, ClientInfo}
  end.
При этом иногда количество аргументов довольно велико. И всё это разрастается в одну большую кашу. И таких блоков под полсотни уже. Я решил, что хорошо бы это хоть как-то объединить в какой-нибудь паттерн.

Для этого я решил использовать макросы. Макрос, по сути, просто заменяет один кусок текста в вашей программе на другой (аналогично #define из C).

Итак, как минимум нам нужно следующее:

  • Идентификатор метода
  • Необходимые аргументы
  • Guard'ы для этих аргументов
  • Функция, в которую они будут переданы
Сделаем это:
-define(METHOD(BinaryString, Arguments, CallbackModule, CallbackFunction),
    request(BinaryString, Args, ClientInfo, MongoSettings) ->
      request_macros(Args, ClientInfo, MongoSettings, Arguments, {CallbackModule, CallbackFunction})
).
Тем самым мы определяем макрос METHOD, принимающий четыре аргумента: идентификатор метода, список его аргументов (об этом чуть далее), модуль и функция, которую нужно будет запустить.

Итак, формат аргументов будет следующим:
[ {<<"arg-name">>, [ guard1, guard2, ... , guardN]} ]
Формат Guard'а (извиняюсь, не знаю, как правильно перевести, не защитник же):
{module, function}
Где module — имя модуля, function — имя функции, которая принимает аргумент и возвращает либо true, либо false.

request_macros — наш вспомогательный метод, который будет собственно и реализовывать все эти проверки.

Его код:
request_macros(Args, ClientInfo, MongoSettings, Arguments, {CallbackModule, CallbackFunction}) ->
  try lists:foldr(fun({ArgName, Guards}, Filled) ->
      case proplists:get_value(ArgName, Args, undefined) of
        undefined -> % Если в proplist'е нет такого аргумента, то выходим из foldr'а
          throw(missmatch);
        Value ->
          % проходимся по списку гвардов, проверяем каждое значение,
          % если возвращает false — покидаем foldr, иначе заполняем список аргументами
          lists:foreach(fun({Module, Function}) ->
              CheckGuard = apply(Module, Function, [Value]),
              if
                not CheckGuard ->
                  throw(missmatch);
                true ->
                  ok
              end
          end, Guards),
          [Value | Filled]
      end
  end, [], Arguments) of
    % Если всё ок и ошибки нет, просто вызываем функцию, которую должны
    CommandArgs ->
      apply(CallbackModule, CallbackFunction, CommandArgs ++ [ClientInfo, MongoSettings]),
  catch
    % В случае, если что-то не сошлось, говорим, что запрос плохой
    throw:missmatch ->
      {error_response, ClientInfo}
  end.
Я думаю, что комментарии дают понять, что делает данная функция. Я поясню только две вещи.

Первое это то, что вместо foldl я использовал foldr, чтобы просто можно было написать [El | Arr], а не Arr ++ [El], по сути, я думаю, не особо важно, что тут использовать. Если нет, сообщите об этом, буду рад.

Ну и второе — довольно долго я не знал, как же выйти из foreach'а, fold[l | r], прочих таких обходов. И вот, пришла мысль, что можно же просто повалить этот обход исключением и словить его выше. Вот так вот.

Ну и как итог, вместо монструозных методов у меня вышли довольно небольшие (в среднем 1-5 строк) вызовы макросов. Т.е. мой пример превращается в:
?METHOD(<<"method1">>, [
            {<<"arg1">>, [{erlang, is_integer}]},
            {<<"arg2">>, [{erlang, is_binary}]}
    ], some_module, some_method);
Довольно локанично, не находите?

Если есть какие-то советы, или ярость, по поводу того, что нельзя так делать или я делаю что-то не так, призываю вас в комментарии.

Спасибо.

оставить комментарий

0 коммент.:

Отправить комментарий

Перед тем как прокомментировать, подумай, действительно ли это нужно. На блоге действует постмодерация и монархия.