Created
March 21, 2012 13:30
-
-
Save stolen/2146886 to your computer and use it in GitHub Desktop.
Erlang parse_transform tutorial files
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
*.beam |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Использование 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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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'}] | |
}]. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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}. |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Благодарности безграничны от очарованных и благоговеющих эрлангистов ^_^