Вызов функции через кортеж

    Не так давно наткнулся на одну интересную возможность в Erlang. Если вместо названия модуля при вызове функции передать кортеж, где первый элемент — название модуля, то будет вызвана функция
    арностью на единицу больше вызываемой и последним аргументом будет тот самый кортеж.

    Пример:
    -module(my_module).
    
    -export([test/2]).
    
    test(Arg1,{?MODULE,Arg2}) ->
      io:format("Arg1:~p~nArg2:~p~n",[Arg1,Arg2]).
    

    Можно вызвать как:
    my_module:test(1,{my_module,2}).
    или так
    {my_module,2}:test(1).


    Как это можно применить?

    Например при создании абстрактных типов данных (модели данных) где в кортеже (или, еще удобнее, в record-e) хранятся все данные.
    -module(user).
    -export([new/0,save/1]).
    -export([name/1,set_name/1]).
    
    -export([proplist/1]).
    
    
    % record name must be same as module name.
    -record(user,{id,name}).
    
    new() ->
      {ok,#user{}}.
    
    name(#user{name=Name}) ->
      Name.
    
    set_name(NewName) ->
      {ok,State#user{name=NewName}}.
    
    % … Some other code ...
    
    save(#user{id=undefined,name=Name} = State) ->
      % Create new object in db;
      % ...
      {ok,State};
    save(#user{id=ID,name=Name} = State) ->
      % Update an object in database
      % ...
      {ok,State};
    
    proplist(#user{id=ID,name=Name}) ->
      [{id,ID},
       {name,Name}].
    

    В итоге получается тип user который в коде достаточно удобно использовать:
    {ok,User} = user:new(),
    {ok,User2} = User:set_name("SomeName"),
    {ok,User3} = User2:save(),
    UserName = User3:name().
    

    В принципе идея ясна, но программирование в таком стиле — не Erlang way, и вдобавок осложняется статическая проверка кода Dialyzer. Плюс этот код отличается от такого
    {ok,User} = user:new(),
    {ok,User2} = user:set_name("SomeName",User),
    {ok,User3} = user:save(User2),
    UserName = user:name(User3).
    

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

    Я же в своем проекте использовал эту особенность только из-за возможности создания функции над абстрактным типом данных, зная интерфейс доступа к нему (например, в предыдущей модели в качестве интерфейса выступает функция proplist()).

    При разработке RESTful сервиса очень удобно описать ресурсы моделями, а при отдаче конвертировать в нужные типы не зная какому конкретно модулю принадлежит данная модель.
    
    to_json(Resource) ->
      Proplist = Resource:proplist(),
      Json = mochijson2_fork:encode({struct,[Proplist]}),
      {ok,Json}.
    
    to_xml(Resource) ->
      Proplist = Resource:proplist(),
      XML = SomeProplistToXmlGenerator(Proplist),
      {ok,XML}.
    
    
    Поделиться публикацией
    Комментарии 19
      +1
      Это стоит рассматривать как хак над параметризованными модулями.
        +2
        очень не советую использовать. насколько мне не изменяет память, эту возможность собираются выпилить в R16
            +1
            На сколько я понял это не совсем то.
            tuple fun это как-то так:
            F = {lists,seq},
            F(1,10).
            

              0
              Эта ссылка правильная: erlang.org/pipermail/erlang-questions/2012-January/063906.html
                0
                Это уже интереснее: могут выпилить, но могут таки и оставить, слегка улучшив :)
                Не знаю как к этому относиться: с одно стороны не хочется чтобы Erlang потерял свою лаконичность из-за введения новых фич; с другой-же, складывается ощущение, что в нем чего-то таки не хватает. Какой-то мелочи, которая сделала бы его идеальным, по крайне мере для меня.
                Но с тем что record не самое лучшее решение (особенно, из-за того что его структура теряется в runtime, что особенно критично когда State хранится в record и при добавлении нового поля при hot code reload все летит к чертям ) и его надо чем-то заменить, я полностью согласен.
                  +1
                  Специально для вас в OTP есть хуки на смену кода. Там можно делать миграцию рекордов (правда, придется тащить за собой предыдущие версии).
                    0
                    что в нем чего-то таки не хватает.

                    Статической типизации, каррирования, монад)) Но как это обойти во общем-то понятно. Но это тоже «черная магия».
                    State хранится в record

                    А чем Вам proplists не угодили? Они конечно медленее, но тут применимы.
                      0
                      Тут применимы, я имею в виду более сложные случаи, в которых производительность — ключевой фактор.
                      +proplist нельзя match-ить
                        0
                        it depends в обоих случаях.

                        А потом, по сути все равно будет О(1), ибо ««вряд ли придет в голову динамически создавать десятки сотни, тысячи полей.
                        Проплисты — можно матчить как обычные списки, возможно придется учитывать порядок следования, но если производительность важнее, то можно и потерпеть такие неудобства.

                        В глубине души я с Вами согласен. Но хочется иметь возможность обойти такие неудобства без недокументированных возможностей языка.
                        А если использовать параметризованные модули, то лучше это делать явно.
                        Тогда появляется возможность использовать extend. Тоже спорная, но иногда удобная вещь.
                        +1
                        каррирования, монад)) Но как это обойти во общем-то понятно. Но это тоже «черная магия».

                        Erlando наше все :)
                          0
                          черная магия parse_transfotm
                  0
                  Кстати, даже если удалят, то эту идею можно применить немного по-другому. Если состояние хранится в картеже где первый элемент имя модуля, то можно будет код модифицировать как-то так :)

                  to_json(Resource) ->
                    ResModule = element(1),
                    Proplist = ResModule:proplist(Resource),
                    Json = mochijson2_fork:encode({struct,[Proplist]}),
                    {ok,Json}.
                  
                  to_xml(Resource) ->
                    ResModule = element(1),
                    Proplist = ResModule:proplist(Resource),
                    XML = SomeProplistToXmlGenerator(Proplist),
                    {ok,XML}.
                  
                    0
                    erlang:element(1, Resource). 
                    
                    ?

                    Имхо, не очевидные вещи. + Закладка на порядок. Это, таки, эффективно, но совсем не расширяемо.
                    Еще раз объясните, пожалуйста, зачем все это нужно и что мы с этого имеем? Почему нельзя просто вызвать модуль статические, или например передать его вторым параметром?
                      0
                      прошу прощения… описался.
                      На самом деле это не более чем синтакчический сахар, чтобы не светить явно меркер пренадлежности структуры модулю.
                      Т.е. если мы знаем, что в функцию бедет передаваться структура данных, модуль отвечающей за котороую поддерживает функцию some_function можно писать так
                      my_fun(Data) ->
                         Res = Data:some_function(),
                         % process data 
                         ...
                      

                      вместо
                      my_fun({Module,Data}) ->
                         Res = Module:some_function(Data),
                         % process data 
                         ...
                      

                      Иногда это просто удобнее…

                        +1
                        Я придумал, как всех помирить при помощи лямбды.

                        -module(no_par_mod).
                        -compile(export_all).
                        
                        -spec make_caller(Module::atom(), Data::any()) -> fun((Function::atom(), Args::[any()]) -> any()).
                        make_caller(Module, Data) ->
                           fun(Function, Args) ->
                              erlang:apply(Module, Function, [Data|Args])
                           end.
                        
                        hello(Instance, X, Y) ->
                           io:format("Hello, ~s, take these ~w and ~w!~n", [Instance, X, Y]).
                        
                        caller_demo() ->
                           Instance = "Kitty",
                           Caller = make_caller(?MODULE, Instance),
                           Caller(hello, [apple, orange]).
                        
                          0
                          Так немного красивее:
                          caller_demo() ->
                             Kitty = make_caller(?MODULE, "Kitty"),
                             Kitty(hello, [apple, orange]).
                          
                  0
                  у вас уже r16?
                    0
                    не угадал с веткой

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое