Skip to content

Instantly share code, notes, and snippets.

@w495
Created June 12, 2012 18:31
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save w495/2919244 to your computer and use it in GitHub Desktop.
Save w495/2919244 to your computer and use it in GitHub Desktop.
Декораторы в Erlang

Перевод статьи: http://niki.code-karma.com/2011/06/python-style-decorators-in-erlang/

Введение

Алан Перлис однажды сказал, «Не стоит изучать язык, который не меняет вашего представления о программировании.» Мои любимые языки Erlang, Python и C++ (а еще Lua, но сейчас Python более востребован на рынке). Я люблю их за разное, и все они имеют совершенно разные сильные и слабые стороны и особенности.

Также, я люблю размышлять от том, можем ли мы и каким образом применить в одном языке идеи, которые привычны для другого языка. И если есть какие-либо преимущества в этом. Сегодня я собираюсь рассмотреть декораторы в стиле Python реализованные на Erlang.

Вот вам пример кода:

https://github.com/nbowe/erlang_decorators/blob/master/test/decorator_test.erl


Давайте вспомним, что декораторы Python не имеют ничего общего с шаблоном проектирования «Декоратор» Банды Четырех. Декораторы, это синтаксический сахар Python. Они позволяют применять одну функцию к другой. Например:

@memoize
def some_expensive_func(n):
    return some_expensive_computation(n)

тоже самое, что и

def some_expensive_func(n):
    return some_expensive_computation(n)
some_expensive_func = memoize(some_expensive_func)

А все потому, что в Python функции являются объектами первого класса. Декораторы — обычный прием для Python, и один из тех, которые мне действительно нравятся. Я лично думаю, что при правильном использовании они делают функции понятнее. Декораторы позволяют выделить основную суть, того что делает функция?

Вы можете больше узнать о происхождении синтаксиса декораторов тут: http://www.python.org/dev/peps/pep-0318/

Увидеть несколько хороших примеров использования декораторов можно тут: http://wiki.python.org/moin/PythonDecoratorLibrary

Декораторы также очень распространены в одном из моих любимых веб фреймворков, Django. Следуя изложенному в PEP я решил поставить следующие задачи:

  1. Позволить декораторам выполнять код до и после вы вызова целевой функции. Например, для выполнения транзацкий базы данных, кеширования значений, трассировки и профилирования выполнения.
  2. Позволить декораторам менять список аргументов, которые передаются целевой функции.
  3. Использовать простой не навязчивый синтаксис, в пределах синтаксических правил Erlang.
  4. Сделать возможным использовать несколько декораторов для одной целевой функции.
  5. Поддерживать параментризованные декораторы.

Erlang не поддерживает изменение значений переменных. Потому никаких фокусов и превращений (хаков, с подменами методов и аргументов) (прим.перев. "monkey business" \ "monkey patching" не возможно в русском языке передать тонкий австралийский юмор автора).

Erlang также не поддерживает выполнение в области видимости модуля. Хотя, в этом сдучае, применение функции on_load позволяет достичь аналогичных результатов. Но это немного усложняет реализацию. Я решил попробовать решить задачи с помощью parse_transform.

План

-decorate( {decorator_module, decorator_function} ).
foo(N) -> N.

заменять на

foo(N) -> foo_arity0_1([N]).
foo_arity0_1(Args) ->
    F = decorator_module:decorator_function( fun foo_arity0_0/1, Args),
    F().
foo_arity0_0([N]) -> foo_original___(N).
foo_original___(N) -> N.

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

Наш первый тест

Как большой поклонник TDD я написал очень простой первый тест, который можно использовать для того, чтобы начать разработку.

-module(decorator_test).
-include_lib("eunit/include/eunit.hrl").
-export([replace_return_value_decorator/2]).

-compile([{parse_transform, decorators}]).

% пример декоратора, который заменяет возвращаемое значение на атом 'replaced'
replace_return_value_decorator(F,Args)->
    fun() ->
        _R = apply(F, [Args] ),
        replaced
    end.

-decorate({ ?MODULE, replace_return_value_decorator }).
replace_ret_val_decorated() -> ok.

replace_ret_value_test()->
    ?assertEqual(replaced, replace_ret_val_decorated() ).

Получение абстрактной формы

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

parse_transform(Ast,_Options)->
    io:format("~p~n=======~n",[ Ast ]),
    Ast.

Мы получили некоторые выходные данные. Прочитать подробнее про формат можно тут: http://www.erlang.org/doc/apps/erts/absform.html

[{attribute,1,file,{"test/decorator_test.erl",1}},
    {attribute,1,module,decorator_test},
    {attribute,0,export,[{test,0},{replace_ret_value_test,0}]},
    {attribute,1,file,
    {"c:/PROGRA~2/ERL57~1.5/lib/eunit-2.1.5/include/eunit.hrl",1}},
    {attribute,3,file,{"test/decorator_test.erl",3}},
    {attribute,3,export,[{replace_return_value_decorator,2}]},
    {attribute,5,compile,[]},
    {function,10,replace_return_value_decorator,2,
    [{clause,10,
        [{var,10,'F'},{var,10,'Args'}],
        [],
        [{'fun',11,
        {clauses,
        [{clause,11,[],[],
            [{match,12,
            {var,12,'_R'},
            {call,12,
                {atom,12,apply},
                [{var,12,'F'},{cons,12,{var,12,'Args'},{nil,12}}]}},
            {atom,13,replaced}]}]}}]}]},
    {attribute,16,decorate,{decorator_test,replace_return_value_decorator}},
    {function,17,replace_ret_val_decorated,0,[{clause,17,[],[],[{atom,17,ok}]}]},
    {function,19,replace_ret_value_test,0,
    [{clause,19,[],[],
        [{call,20,
        {'fun',20,
        {clauses,
            [{clause,20,
            [{var,20,'__X'}],
            [],
            [{'case',20,
                {call,20,{atom,20,replace_ret_val_decorated},[]},
                [{clause,20,[{var,20,'__X'}],[],[{atom,20,ok}]},
                {clause,20,
                [{var,20,'__V'}],
                [],
                [{call,20,
                    {remote,20,
                    {record_field,20,{atom,20,''},{atom,20,erlang}},
                    {atom,20,error}},
                    [{tuple,20,
                    [{atom,20,assertEqual_failed},
                    {cons,20,
                        {tuple,20,[{atom,20,module},{atom,20,decorator_test}]},
                        {cons,20,
                        {tuple,20,[{atom,20,line},{integer,20,20}]},
                        {cons,20,
                        {tuple,20,
                        [{atom,20,expression},
                            {string,20,"replace_ret_val_decorated ( )"}]},
                        {cons,20,
                        {tuple,20,[{atom,20,expected},{var,20,'__X'}]},
                        {cons,20,
                            {tuple,20,[{atom,20,value},{var,20,'__V'}]},
                            {nil,20}}}}}}]}]}]}]}]}]}},
        [{atom,20,replaced}]}]}]},
    {eof,22},
    {function,0,test,0,
    [{clause,0,[],[],
        [{call,0,
        {remote,0,{record_field,0,{atom,0,''},{atom,0,eunit}},{atom,0,test}},
        [{atom,0,decorator_test}]}]}]}]

Pretty Printing

Я думаю, что абстрактные формы довольно просты для понимания. Но было бы лучше иметь возможность видеть, то что мы делаем, как если бы это был обычный код на Erlang. К счастью, erl_pp умеет это.

Если вы будете писать собственную трансформацию, то для отладки будет удобно использовать функцию:

pretty_print(Ast) ->
    lists:flatten([erl_pp:form(N) || N<-Ast]).

Пропустив абстрактное дерево через эту функцию мы получим:

-file("test/decorator_test.erl", 1).
-module(decorator_test).
-export([test/0,replace_ret_value_test/0]).
-file("c:/PROGRA~2/ERL57~1.5/lib/eunit-2.1.5/include/eunit.hrl", 1).
-file("test/decorator_test.erl", 3).
-export([replace_return_value_decorator/2]).
-compile([]).
replace_return_value_decorator(F, Args) ->
    fun() ->
        _R = apply(F, [Args]),
        replaced
    end.
-decorate({decorator_test,replace_return_value_decorator}).
replace_ret_val_decorated() ->
    ok.
replace_ret_value_test() ->
    fun(__X) ->
        case replace_ret_val_decorated() of
            __X ->
                ok;
            __V ->
                .erlang:error({assertEqual_failed,
                                [{module,decorator_test},
                                {line,20},
                                {expression,
                                    "replace_ret_val_decorated ( )"},
                                {expected,__X},
                                {value,__V}]})
        end
    end(replaced).

test() ->
    .eunit:test(decorator_test).

Трансформация

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

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

Нам также будет нужно удалить атрибуты декораторов. Как если бы Вы только подразумевали иметь атрибуты перед всеми функциями.

parse_transform(Ast,_Options)->
    %io:format("~p~n=======~n",[Ast]),
    %io:format("~s~n=======~n",[pretty_print(Ast)]),
    {Extended_ast2, Rogue_decorators} = lists:mapfoldl(fun transform_node/2, [], Ast),
    Ast2 = lists:flatten(lists:filter(fun(Node)-> Node =/= nil end, Extended_ast2)),
    %io:format("~p~n<<<<~n",[Ast2]),
    %io:format("~s~n>>>>~n",[pretty_print(Ast2)]),
    Ast2.
%
% преобразует узлы уровня модуля
% см http://www.erlang.org/doc/apps/erts/absform.html
% возвращает пустой узел (nil),
% единственный узел, или список узлом.
% пустые узлы удаляются при следующем проходе и списки спрямляются (flatten).
%
transform_node(Node={attribute, _line, decorate, _decorator}, Dlist) ->
    % собирает список декораторов (Dlist), но не выводит их в результурающий код.
    % это важно, потому что атрибуты не должны идти после функций
    {nil, [Node|Dlist]};
transform_node(Node={function, _line, _fname, _arity, _clauses}, []) ->
    % пропускает функциии без декораторов
    {Node, []};
transform_node(Node={function, _line, _fname, _arity, _clauses}, Dlist) ->
    % применяет декораторы к этой функции и обнуляет список декораторов
    {apply_decorators(Node,Dlist), []};
transform_node(Node, Dlist) ->
    % любая другая правильная форма и другие атрибуты.
    {Node, Dlist}.

apply_decorators(Node={function, Line, Fname, Arity, _clauses}, [_d|_]=Dlist) ->
    [
        % Оригинальная, переименованная функция.
        function_form_original(Node),
        % Замена оригинальной функции на нашу цепочку декораторов.
        function_form_trampoline(Line, Fname, Arity, Dlist),
        % Функция funname_arityn_0 для преобразования
        % входных параметров в единый список.
        function_form_unpacker(Line,Fname,Arity)
        % Цепочка декораторов.
        | function_forms_decorator_chain(Line, Fname, Arity, Dlist)
    ].

(прим. перев.: я позволил себе некоторые стилистические правки в коде, потому что, по моему скромному мнению, он стал более читаемым.)

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

function_form_decorator_chain(Line,Fname,Arity, {Dmod, Dfun}, Dindex) ->
    Next_fname = generated_func_name({decorator_wrapper, Fname, Arity, Dindex-1}),
    {function, Line,
        generated_func_name({decorator_wrapper, Fname,Arity, Dindex}), % name
        1, % арность
        [{ clause, Line,
            emit_arguments(Line, ['ArgList'] ),
            emit_guards(Line, []),
            [
                % F = Dmod:Dfun( fun NextFun/1, ArgList),
                emit_decorated_fun(Line, 'F', {Dmod, Dfun}, Next_fname, 'ArgList'),
                % call 'F'
                {call, Line,{var,Line,'F'},[]}
            ]
        }]
    }.

Конечно, есть еще нескрлько функций с преффиксом emit_. Но любой, кто хотя бы просматривал докуметацию по абстрактным формам, или абстрактные формы каких либо простых функций сможет собрать все воедино.

Я часто ловлю себя на мысли: "Я не могу поверить, насколько хорош Erlang!" (Python тоже).

Пользовательские предупреждения и ошибки

Ах да, я также хотел бы вывести предупреждение если есть декораторы в конце файла, которые не будут ассоциированы с фунцкциями. Для этого я всего лишь добавил узлы предупреждения (warning nodes) в конец файла для каждого элемента Rogue_decorators.

parse_transform(Ast,_Options)->
    {Extended_Ast2, Rogue_decorators} = lists:mapfoldl(fun transform_node/2, [], Ast),
    Ast2 = lists:flatten(lists:filter(fun(Node)-> Node =/= nil end, Extended_Ast2))
        ++ emit_errors_for_rogue_decorators(Rogue_decorators),
    Ast2.

emit_errors_for_rogue_decorators(Dlist)->
    [
        {
           error,
           {
               Line,
               erl_parse,
               ["rogue decorator ", io_lib:format("~p",[D])]
           }
        }
        || {attribute, Line, decorate, D} <- Dlist
    ].

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

transform_node(Node={eof,_Line}, DecoratorList) ->
    {[Node| emit_errors_for_rogue_decorators(DecoratorList) ], []};

Добавление

-decorate({f,f}).

в конец файла приводит к следующему:

$ ./rebar compile eunit
==> erlang_decorators (compile)
==> erlang_decorators (eunit)
test/decorator_test.erl:22: rogue decorator {f,f}

Результат

В результате мы получили код, который можно найти тут: https://github.com/nbowe/erlang_decorators Возможно его следует доработать или немного почистить.

Вот что мы получим после преобразования (т.е. после pretty_print (Ast2)):

-file("test/decorator_test.erl", 1).
-module(decorator_test).
-export([test/0,replace_ret_value_test/0]).
-file("c:/PROGRA~2/ERL57~1.5/lib/eunit-2.1.5/include/eunit.hrl", 1).
-file("test/decorator_test.erl", 3).
-export([replace_return_value_decorator/2]).
-compile([]).
replace_return_value_decorator(F, Args) ->
    fun() ->
        _R = apply(F, [Args]),
        replaced
    end.
replace_ret_val_decorated_original___() ->
    ok.
replace_ret_val_decorated() ->
    replace_ret_val_decorated_arity0_1([]).
replace_ret_val_decorated_arity0_0([]) ->
    replace_ret_val_decorated_original___().
replace_ret_val_decorated_arity0_1(ArgList) ->
    F = decorator_test:replace_return_value_decorator(fun replace_ret_val_decora
ted_arity0_0/1,
                                                    ArgList),
    F().
replace_ret_value_test() ->
    fun(__X) ->
        case replace_ret_val_decorated() of
            __X ->
                ok;
            __V ->
                .erlang:error({assertEqual_failed,
                                [{module,decorator_test},
                                {line,20},
                                {expression,
                                    "replace_ret_val_decorated ( )"},
                                {expected,__X},
                                {value,__V}]})
        end
    end(replaced).

test() ->
    .eunit:test(decorator_test).

Лекго видеть, мы получили то что и планировали. И в результате:

$ ./rebar compile eunit
==> erlang_decorators(compile)
Compiled src/decorators.erl
Compiled src/decorators_app.erl
Compiled src/decorators_sup.erl
==> erlang_decorators(eunit)
Compiled src/decorators.erl
Compiled src/decorators_app.erl
Compiled src/decorators_sup.erl
Compiled test/decorator_test.erl
All 4 tests passed.

Думаю достаточно.

А полезно ли это?

Это просто синтаксический сахар, что верно и для Python. Если вы хотите знать, где используются декораторы — можно взглянуть на Django.

Мне было интересно поработать в parse_transform Кроме того, был получен интересный вариант синтаксиса.

Лично мне нравится такой вариант декоратора, но на вкус и цвет товарищей нет. Особенно в языках программирования. Я считаю, что декораторы могут быть полезны для разделения ответвенности. При их использовании не нужно засорять функции дополнительной логикой.

После этого я думаю, сделать более общую систему атрибутов общего назначения. http://msdn.microsoft.com/en-us/library/aa288059%28v=vs.71%29.aspx

Я думаю, это можно достаточно доработать, чтобы, например, автоматически создавать определенные функции, такие как вызовы удаленных процедур на основе декораторов.

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

Ограничения и возможные расширения

Я не до конца реализовал передачу аргументов в декоратор, но это сравнительно просто.

Erlang является функциональным языком, с единичным присваиванием. Если вы не используете динамеческую генерацию модульей (например, meck https://github.com/eproxus/meck), вы не можете переопределить функции.

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

В будущем можно было бы добавить поддержку генерации функции on_loaded или возможности запрашивать у модуля его абстрактные формы без включения флага компиляции debug_info. Оба подхода могут пригодиться для разрешения этого ограничения.

Мы все еще остаемся в рамках синтаксических правил Erlang. Потому нельзя декорировать только одно уравнение в определении функции.

bar(1) -> ok;
-decorate({x,y}).
bar(N) -> bar(N-1).

Если подойти к вопросу серьезнее, то можно было бы сделать возможным более простое определение произвольных декораторов в заголовке модуля. Это сильно упростило бы синтаксис. И конечно реализовать передачу дополнительной информации в декоратор. Что-то вроде:

-module(foo).
-include("logger_decorator.hrl").

-log_entry_exit( [{logger, logger_proc }] ).
foo() -> 1.
@alexanius
Copy link

Я также люблю размышлять от том, что если применить в одном языке идеи (идиомы), которые привычны для другого языка.
< Не сразу понял фразу. "Я также люблю размышлять от том, можем ли мы и каким образом применить в одном языке идеи, которые привычны для другого языка."

Давайте вспомним, что декораторы Python не имеют ничего общего с шаблоном проектирования «Декоратор».
< А банду четверых (GoF) куда зажал? :)

Декораторы — обычные прием для Python,
< склонение s/прием/приемы/ либо s/обычные/обычный/

Декораторы позволяют выделить основную суть, того что делает функция?
< вынесением мелких деталей из основной логики.

множественное присваивание
< лично я бы перевёл как "изменение значения", ибо очень ассоциируется с этим http://ru.diveintopython.net/odbchelper_multiassign.html

достич
< достичь

< потерян заголовок "Getting the abstract forms"

Сначала, я сделал самую простую вещь, какая только возможно
< s/возможно/возможна/

< потерян заголовок "Pretty Printing"

как вы на самом деле предназначены только иметь атрибуты перед всеми функциями
< как если бы Вы только подразумевали иметь атрибуты перед всеми функциями

узелы предупреждения
< узлы предупреждения

Поэтому я добавил следующее уравнение в transform_node
< а "clause" в контексте erlang реально переводится как "уравнение"?

Вот что мы получим после обратного преобразования
< Heres the code after transformation. Обратного?

Мне лично нравится такой вариант декоратора На вкус и цвет
< точка пропущена

Ограничение и возможные расширения
< Ограничения и возможные расширения

@w495
Copy link
Author

w495 commented Jun 13, 2012 via email

@superbobry
Copy link

Извиняюсь за опоздание, я таки добрался до черновика:

Я лично думаю, что при правильном использовании они делают функции понятнее.

Скорей при правильном использовании они не сильно усложняют понимание результата.

Декораторы позволяют выделить основную суть, того что делает функция

Довольно спорное утверждение -- оно было в оригинале? в общем случае декоратор вообще не имеет ничего общего с тем, что делает функция, напр. тот же @memoize.

В целом, "по сути" у меня замечаний нет, перечислять опечатки и стилистические неровности наверное смысла не много. Сама же идея применения декораторов в Erlang мне кажется довольно странной, потому что:

  • это та фича которую легко абъюзить
  • мотивация и преимущества введения нового синтаксиса как минимум не очевидны -- стоит ли вся эта черная магия с parse_transform потенциальной потери читаемости?

Посему, надо либо добавить своих убедительных примеров, либо просто не публиковать статью, дабы не смущать неокрепшие умы юных эрлангеров.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment