Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@stolen
Created March 21, 2012 13:30
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save stolen/2146886 to your computer and use it in GitHub Desktop.
Save stolen/2146886 to your computer and use it in GitHub Desktop.
Erlang parse_transform tutorial files
Использование parse_transform
Disclaimer: Описываемый инструмент имеет спорную репутацию. Я не призываю использовать
его где ни попадя, только знакомлю с используемыми понятиями,
дабы уменьшить некоторым трепет перед технологией.
Что такое parse_transform
parse_transform — механизм изменения AST перед компиляцией. Предназначен для
изменения значения конструкций (семантики), не выходя за синтаксис Эрланга.
К сожалению, в Сети мало информации про это, что делает порог вхождения весьма высоким для не-гуру эрланга.
Что мы будем делать
В рамках данной статьи я немного рассказажу про AST эрланга, приведу пример
простых трансформаций, а так же покажу процесс написания parse_transform
для создания stateless gen_server-а (задача имеет не особо много смысла,
но в качестве примера использования сгодится), а в конце дам ссылку на набор начинающего транформатора.
AST в Эрланге
На всякий случай: определение AST
Лучше один раз увидеть AST, чем сто раз прочитать его описание. Поэтому мы напишем
маленький модуль, чтобы увидеть, как преоразуется каждая строчка.
Итак, исходный текст astdemo.erl:
-module(astdemo).
-export([hello/0, hello/2]).
hello() ->
hello("world", 1).
hello(_What, 0) ->
ok;
hello(What, Count) ->
io:format("Hello, ~s~n", [What]),
hello(What, Count - 1).
Чтобы увидеть AST, нужно натравить на этот файл функцию parse_file из модуля epp:
Eshell V5.8.5 (abort with ^G)
1> {ok, Forms} = epp:parse_file("astdemo.erl", [], []), io:format("~p~n", [Forms]).
[{attribute,1,file,{"astdemo.erl",1}},
{attribute,1,module,astdemo},
{attribute,2,export,[{hello,0},{hello,2}]},
{function,4,hello,0,
[{clause,4,[],[],
[{call,5,
{atom,5,hello},
[{string,5,"world"},{integer,5,1}]}]}]},
{function,7,hello,2,
[{clause,7,[{var,7,'_What'},{integer,7,0}],[],[{atom,8,ok}]},
{clause,9,
[{var,9,'What'},{var,9,'Count'}],
[],
[{call,10,
{remote,10,{atom,10,io},{atom,10,format}},
[{string,10,"Hello, ~s~n"},
{cons,10,{var,10,'What'},{nil,10}}]},
{call,11,
{atom,11,hello},
[{var,11,'What'},
{op,11,'-',{var,11,'Count'},{integer,11,1}}]}]}]},
{eof,12}]
ok
Видно, что каждое выражение преобразуется в тюпл длины не менее 3, при этом первые два элемента
всегда тип и строка, далее идет специфическое для него описание. Если непонятно,
что стоит на конкретном месте, документация к вашим услугам.
Функция parse_transform/2
Давайте теперь сделаем dummy-parse_transform, чтобы увидеть, с чем придется иметь дело дальше.
Для этого создадим модуль, который займется трансформацией,
и вместо манипуляций над AST просто распечатаем его.
Итак, demo_pt.erl:
-module(demo_pt).
-export([parse_transform/2]).
parse_transform(Forms, _Options) ->
io:format("~p~n", [Forms]),
Forms.
Вставляем в astdemo.erl соответствующую директиву:
-module(astdemo).
-compile({parse_transform, demo_pt}).
-export([hello/0, hello/2]).
...........
Компилируем:
Eshell V5.8.5 (abort with ^G)
1> c(astdemo).
[{attribute,1,file,{"./astdemo.erl",1}},
{attribute,1,module,astdemo},
{attribute,3,export,[{hello,0},{hello,2}]},
{function,5,hello,0,
[{clause,5,[],[],
[{call,6,
{atom,6,hello},
[{string,6,"world"},{integer,6,1}]}]}]},
{function,8,hello,2,
[{clause,8,[{var,8,'_What'},{integer,8,0}],[],[{atom,9,ok}]},
{clause,10,
[{var,10,'What'},{var,10,'Count'}],
[],
[{call,11,
{remote,11,{atom,11,io},{atom,11,format}},
[{string,11,"Hello, ~s~n"},
{cons,11,{var,11,'What'},{nil,11}}]},
{call,12,
{atom,12,hello},
[{var,12,'What'},
{op,12,'-',{var,12,'Count'},{integer,12,1}}]}]}]},
{eof,13}]
{ok,astdemo}
Как видно, AST тот же самый (с точностью до смещения строк), но в этот раз он распечатан во время компиляции.
Следует отметить, что в прибывшем для трансформации AST уже удалены директивы компилятора.
Что передается в опциях, любознательный читатель, вероятно, узнает самостоятельно. Эта статья об AST.
Первые трансформации
Давайте для тренировки сделаем бесполезную на практике вещь — переименуем функцию «hello/0» в «hi/0».
Это будет просто сделать, поскольку hello/0 не вызывается изнутри модуля, а имеет только возможность
быть вызванной извне. Поэтому достаточно изменить список экспортов и заголовок функции.
Трансформатор одной формы
Поскольку AST (биндинг Forms) является списком, каждый элемент которого является формой
очень короткого списка типов, логично пропустить все Forms через функцию-мутатор.
Поскольку поставленная задача проста, и трансформация каждого выражения не зависит от
остального содержимого, нам подойдет lists:map.
Функция, которая будет изменять экспорты и заголовки функций, будет выглядеть примерно так:
% hello_to_hi replaces occurences of hello/0 with hi/0
hello_to_hi({attribute, Line, export, Exports}) ->
% export attribute. Replace {hello, 0} with {hi, 0}
HiExports = lists:map(
fun ({hello, 0}) -> {hi, 0};
(E) -> E
end, Exports),
{attribute, Line, export, HiExports};
hello_to_hi({function, Line, hello, 0, Clauses}) ->
% Header of hello/0. Just replace hello with hi
{function, Line, hi, 0, Clauses};
hello_to_hi(Form) ->
% Default: do not modify form
Form.
Теперь всё вместе
Задействуем эту функцию, изменив код функции parse_transform:
parse_transform(Forms, _Options) ->
HiForms = lists:map(fun hello_to_hi/1, Forms),
io:format("~p~n", [HiForms]),
HiForms.
Компилируем demo_pt, удостоверяемся, что не накосячили.
Проверяем
Пробуем с новым трансформатором скомпилировать astdemo:
Eshell V5.8.5 (abort with ^G)
1> c(astdemo).
[{attribute,1,file,{"./astdemo.erl",1}},
{attribute,1,module,astdemo},
{attribute,3,export,[{hi,0},{hello,2}]},
{function,5,hi,0,
[{clause,5,[],[],
[{call,6,
{atom,6,hello},
[{string,6,"world"},{integer,6,1}]}]}]},
{function,8,hello,2,
[{clause,8,[{var,8,'_What'},{integer,8,0}],[],[{atom,9,ok}]},
{clause,10,
[{var,10,'What'},{var,10,'Count'}],
[],
[{call,11,
{remote,11,{atom,11,io},{atom,11,format}},
[{string,11,"Hello, ~s~n"},
{cons,11,{var,11,'What'},{nil,11}}]},
{call,12,
{atom,12,hello},
[{var,12,'What'},
{op,12,'-',{var,12,'Count'},{integer,12,1}}]}]}]},
{eof,13}]
{ok,astdemo}
2> astdemo:hi().
Hello, world
ok
Прекрасно! Отработало, как и хотели. Время сделать что-то чуть более полезное.
Stateless gen_server parse_transform
Иногда при написании модуля с поведением gen_server нет нужды таскать за собой State,
поскольку хранить в нем нечего, а протаскивание State из handle_anything в финальное
выражение засоряет код. Давайте сделаем parse_transform, который позволит определять
handle_call/2, handle_cast/1, handle_info/1. Или нет. Чтобы сделать статью чуть короче,
я покажу только трансформацию handle_call/2 -> handle_call/3, а те, кому интересно, доопределят все остальное.
Концепция
Поведение gen_server требует определения handle_call (для простоты) таким образом (документация):
handle_call(Request, From, State) ->
.....
{reply,Reply,NewState}.
Поскольку мы избавляемся от необходимости учитывать State, пусть наш синтаксис будет таким:
handle_call(Request, From) ->
.....
Reply.
План трансформации
Найти и изменить в экспортах handle_call/2 на handle_call/3
Среди определений функций для handle_call/2 добавить параметр State и финальное выражение
в каждой кляузе обрамить в {reply, ..., State}
Кошка
На ней мы будем тренироваться. Определена handle_call в нашем синтаксисе и ее аналог
в каноническом виде для сравнения и написания трансформатора.
-module(sl_gs_demo).
-behavior(gen_server).
-compile({parse_transform, sl_gs}).
-export([handle_call/2, ref_handle_call/3]).
-export([handle_cast/2, handle_info/2]).
-export([init/1, terminate/2, code_change/3]).
% This will be transformed
handle_call(Req, From) ->
{Req, From}.
% That's what handle_call should finally look like
ref_handle_call(Req, From, State) ->
{reply, {Req, From}, State}.
% Dummy functions to make gen_server happy
% Exercise: Try to insert them automatically during transformations :)
handle_cast(_, State) -> {noreply, State}.
handle_info(_, State) -> {noreply, State}.
init(_) -> {ok, none}.
terminate(_, _) -> ok.
code_change(_, State, _) -> {ok, State}.
Код
Все было написано как и в прошлый раз — глядя на вывод epp:parse_file
и подгоняя то, что есть, под то, что надо.
-module(sl_gs).
-export([parse_transform/2]).
parse_transform(Forms, _Options) ->
lists:map(fun add_missing_state/1, Forms).
add_missing_state({attribute, Line, export, Exports}) ->
% export attribute. Replace {handle_call, 2} with {handle_call, 3}
NewExports = lists:map(
fun ({handle_call, 2}) -> {handle_call, 3};
% You can add more clauses here for other function mutations
(E) -> E
end, Exports),
{attribute, Line, export, NewExports};
add_missing_state({function, Line, handle_call, 2, Clauses}) ->
% Mutate clauses
NewClauses = lists:map(fun change_call_clause/1, Clauses),
% Finally, change arity in header
{function, Line, handle_call, 3, NewClauses};
add_missing_state(Form) ->
% Default
Form.
change_call_clause({clause, Line, Arguments, Guards, Body}) ->
% Change arity in clauses.
NewArgs = Arguments ++ [{var, Line, 'State'}], % Add State argument
% Then replace last statement of each clause with corresponding tuple
NewBody = change_call_body(Body),
{clause, Line, NewArgs, Guards, NewBody}.
change_call_body([Statement | Rest=[_|_] ]) -> % Rest has to be non-empty list for this
% Recurse to change only last statement
[Statement|change_call_body(Rest)];
change_call_body([LastStatement]) ->
% Put it into tuple. Lines are zero to omit parsing LastStatement
[{tuple,0, [{atom,0,reply},
LastStatement,
{var,0,'State'}]
}].
Проверка на работоспособность
Eshell V5.8.5 (abort with ^G)
1> c(sl_gs_demo).
{ok,sl_gs_demo}
2> {ok, D} = gen_server:start_link(sl_gs_demo, [], []).
{ok,<0.39.0>}
3> gen_server:call(D, hello).
{hello,{<0.32.0>,#Ref<0.0.0.83>}}
Успех! Осталось дорисовать сову и выложить на гитхаб.
Итоги
Заинтересованный читатель, надеюсь, познакомился с AST в эрланге, а так же получил
примерное представление о методах его трансформации. Возможно, кто-то впервые узнал о parse_transform.
В статье собрана информация, которой должно хватить, чтобы приступить к написанию
собстенного трансформа. Чуть ниже будет критика и ссылка на полезную для трансформаций библиотеку.
Критика метода
* Во-первых, использование parse_transform (в том случае, если он в отдельном проекте) добавляет
зависимось вашему проекту. В случае с rebar это несмертельно.
* Во-вторых, люди, читающие (и, особенно, редактирующие) такой код, могут не сразу понять концепцию.
Поэтому нужна не только хорошая документация, но и заметная ссылка на нее в начале исходника.
* В-третьих, возможности по написанию собственных диалектов сильно ограничены.
Прежде, чем AST попадет под ваш скальпель, отрабатывает штатный парсер. Поэтому внесение хитрых
ключевых слов и собственных операторов может сломать парсер, сильно усложнив задачу.
Библиотека parse_trans
parse_trans — полезная штука для написания parse_transform-ов. Она позволяет делать рекурсивный
map на дерево, что крайне полезно при модификации выражений на непостоянной глубине. В примерах
есть очень лаконичный способ переписывания оператора «!» на вызов gproc:send.
Here you can find files shown in article on parse_transform
at http://habrahabr.ru/post/140374/
Feel free to modify and publish without any notice, but
it would be great if you leave link to this repo when using its files.
-module(astdemo).
-compile({parse_transform, demo_pt}).
-export([hello/0, hello/2]).
hello() ->
hello("world", 1).
hello(_What, 0) ->
ok;
hello(What, Count) ->
io:format("Hello, ~s~n", [What]),
hello(What, Count - 1).
-module(demo_pt).
-export([parse_transform/2]).
parse_transform(Forms, _Options) ->
HiForms = lists:map(fun hello_to_hi/1, Forms),
io:format("~p~n", [HiForms]),
HiForms.
% hello_to_hi replaces occurences of hello/0 with hi/0
hello_to_hi({attribute, Line, export, Exports}) ->
% export attribute. Replace {hello, 0} with {hi, 0}
HiExports = lists:map(
fun ({hello, 0}) -> {hi, 0};
(E) -> E
end, Exports),
{attribute, Line, export, HiExports};
hello_to_hi({function, Line, hello, 0, Clauses}) ->
% Header of hello/0. Just replace hello with hi
{function, Line, hi, 0, Clauses};
hello_to_hi(Form) ->
% Default: do not modify form
Form.
-module(sl_gs).
-export([parse_transform/2]).
parse_transform(Forms, _Options) ->
lists:map(fun add_missing_state/1, Forms).
add_missing_state({attribute, Line, export, Exports}) ->
% export attribute. Replace {handle_call, 2} with {handle_call, 3}
NewExports = lists:map(
fun ({handle_call, 2}) -> {handle_call, 3};
% You can add more clauses here for other function mutations
(E) -> E
end, Exports),
{attribute, Line, export, NewExports};
add_missing_state({function, Line, handle_call, 2, Clauses}) ->
% Mutate clauses
NewClauses = lists:map(fun change_call_clause/1, Clauses),
% Finally, change arity in header
{function, Line, handle_call, 3, NewClauses};
add_missing_state(Form) ->
% Default
Form.
change_call_clause({clause, Line, Arguments, Guards, Body}) ->
% Change arity in clauses.
NewArgs = Arguments ++ [{var, Line, 'State'}], % Add State argument
% Then replace last statement of each clause with corresponding tuple
NewBody = change_call_body(Body),
{clause, Line, NewArgs, Guards, NewBody}.
change_call_body([Statement | Rest=[_|_] ]) -> % Rest has to be non-empty list for this
% Recurse to change only last statement
[Statement|change_call_body(Rest)];
change_call_body([LastStatement]) ->
% Put it into tuple. Lines are zero to omit parsing LastStatement
[{tuple,0, [{atom,0,reply},
LastStatement,
{var,0,'State'}]
}].
-module(sl_gs_demo).
-behavior(gen_server).
-compile({parse_transform, sl_gs}).
-export([handle_call/2, ref_handle_call/3]).
-export([handle_cast/2, handle_info/2]).
-export([init/1, terminate/2, code_change/3]).
% This will be transformed
handle_call(Req, From) ->
{Req, From}.
% That's what handle_call should finally look like
ref_handle_call(Req, From, State) ->
{reply, {Req, From}, State}.
% Dummy functions to make gen_server happy
% Exercise: Try to insert them automatically during transformations :)
handle_cast(_, State) -> {noreply, State}.
handle_info(_, State) -> {noreply, State}.
init(_) -> {ok, none}.
terminate(_, _) -> ok.
code_change(_, State, _) -> {ok, State}.
@mdarin
Copy link

mdarin commented May 17, 2018

Благодарности безграничны от очарованных и благоговеющих эрлангистов ^_^

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