Namdak Tonpa (maxim) wrote,
Namdak Tonpa
maxim

Erlang Metaprogramming Real Practice

... Erlang JavaScript compiler in 30 minutes :)

Не случайно ходит мнение среди ML пацанов, что хорошую вещь макросом не назовут :) LISP пацаны однако пишут на макросах и горя не знают. Есть еще такое мнение, что раз нужно метапрограммирование или макросы, значит стоит задача поддержания гавнокода. У нас же мы опускаемся к метапрораммированию только если сталкиваемся с реальным ужасным кодом. Сегодня мы будем говорит о JavaScript.

Поскольку у меня все для демонстрации 4 августа готово: Erlang PaaS работает, контейнеры создаются, приложения на Xen выполняются, мне doxtop разрешил побаловаться и написать JavaScript компилятор из Эрланге.

Все знают, что каждый уважающий себя человек, когда ему нужно написать что-то на джаваскрипте, он пишет компилятор своего любимого языка в джаваскрипт http://altjs.org/ Однако если в Haskell долине есть вменяемый клиентский язык Elm, то в Эрланг мире почему у всех свернуло голову. Browserl (эмулятор Эрланга), ErlyJS (JavaScript to Erlang WTF?), erljs (Run Erlang in JavaScript) -- это все чудовища рожденные соном разума. Почему-то люди решили что BEAM машина это святой грааль и которую обязательно нужно каждому реализовать в JavaScript. Поэтому все работает медленно, ужасно развесисто и вообще уродливо. После общения с автором Erlang on Xen я окончательно убедился, что виртуальная машина Эриксона то еще легаси с конвертацией байт кода из старого в новый формат про который мало кто знает. Зато приемственность версий чуть ли не от самого рождения c другой стороны.

У нас стоит абсолютно конкретная задача: страницы N2O, которые у нас на сервер-сайде сделать выполняемыми на клиенте, что бы можно было писать на Эрланге на стороне клиента, используя веб-браузерные воркеры как эмуляцию элланг процессов и все fun чтобы разворачивались в JavaScript function. Ну а для паттерн мачинга и тейл рекурсии мы возьмем библиотеки matches.js и tailrec.js.

Попытаемся скомпилировать такую функцию fac.erl:
start() ->
    J = 5,
    N = fac(J),
    console:log("factorial ~p", [J, N]).

fac(0) -> 1;
fac(N) -> N * fac(N-1).


Для этого нам нужно построить AST дерево Эрланговское -- это мы делаем несколькими строчками:
parse(File) ->
    {ok,Content} = file:read_file(File),
    {ok,Tokens,X} = erl_scan:string(binary_to_list(Content)),
    tokens(Tokens,[]).

tokens([],Res) -> Res;
tokens(Tokens,Res) ->
    Fun = fun(X)-> case X of {dot,_} ->false; _  -> true end end,
    Cut = lists:takewhile(Fun,Tokens),
    case Cut of
        [] -> tokens([],Res);
        _ -> [H|T] = lists:dropwhile(Fun,Tokens),
             tokens(T,Res ++ [Cut ++ [H]]) end.

forms(File) -> Forms = [ begin {ok,Form} = erl_parse:parse_form(Line),
                       Form end || Line <- parse(File)], io:format("Forms: ~p",[Forms]).


Получается такое дерево из эрланг кортежей и списков:
164> shen:forms("fac.erl").
Forms: [{function,1,start,0,
            [{clause,1,[],[],
                 [{match,2,{var,2,'J'},{integer,2,5}},
                  {match,3,{var,3,'N'},{call,3,{atom,3,fac},[{var,3,'J'}]}},
                  {call,4,
                      {remote,4,{atom,4,console},{atom,4,log}},
                      [{string,4,"factorial ~p"},
                       {cons,4,{var,4,'J'},{cons,4,{var,4,'N'},{nil,4}}}]}]}]},
        {function,6,fac,1,
            [{clause,6,[{integer,6,0}],[],[{integer,6,1}]},
             {clause,7,
                 [{var,7,'N'}],
                 [],
                 [{op,7,'*',
                      {var,7,'N'},
                      {call,7,
                          {atom,7,fac},
                          [{op,7,'-',{var,7,'N'},{integer,7,1}}]}}]}]}]


Которе прекрасно парсается эрлангом:
function(Name,Args,Clauses) ->
  [ io_lib:format("var ~s = pattern({~n", [ Name ]),
    string:join([ clause(Args,C) || C <- Clauses ],",\n"),
    io_lib:format("~s~n",["});"]) ].

clause(Argc,{clause,X,Argv,Guard,Expressions}) ->
    Match = string:join([ exp(Arg) || Arg <- Argv ],","),
    Args = string:join([ arg(Arg,N) || {Arg,N} <- lists:zip(Argv,lists:seq(1,Argc))],","),
  [ io_lib:format("\t'~s': function(~s) {~n", [Match,Args]),
    ["\t\t"++case N == length(Expressions) of true -> "return "; _ -> "" end ++ exp(E)++";\n"
       || {E,N} <- lists:zip(Expressions,lists:seq(1,length(Expressions)))],
    io_lib:format("~s",["\t}"]) ].

arg({integer,X,Value},N) ->           io_lib:format("x~s",[integer_to_list(N)]);
arg({var,X,Value},N) ->               io_lib:format("~s",[string:to_lower(atom_to_list(Value))]).
par(List) ->                          io_lib:format("~s",[lists:flatten(string:join([exp(V)||V<-List],","))]).
exp({nil,X}) -> "[]";
exp({integer,X,Value}) ->             io_lib:format("~s",[integer_to_list(Value)]);
exp({string,X,Value}) ->              io_lib:format("'~s'",[Value]);
exp({cons,X,Left,Right}) ->           io_lib:format("[~s,~s]",[exp(Left),exp(Right)]);
exp({var,X,Value}) ->                 io_lib:format("~s",[string:to_lower(atom_to_list(Value))]);
exp({op,X,'-',Left,Right}) ->         io_lib:format("~s - ~s",[exp(Left),exp(Right)]);
exp({op,X,'*',Left,Right}) ->         io_lib:format("~s * ~s",[exp(Left),exp(Right)]);
exp({call,X,{atom,Y,Name},Params}) -> io_lib:format("~s(~s)",[Name,par(Params)]);
exp({match,X,Left,Right}) ->          io_lib:format("~s = ~s",[exp(Left),exp(Right)]);
exp({call,X,{remote,XX,{atom,Y,Module},{atom,Z,Name}},Params}) -> 
    io_lib:format("~s.~s(~s)",[Module,Name,par(Params)]);
exp(X) -> X.

Обернем все в пролог и эпилог:
prelude() -> io_lib:format("~n~s~n",["var pattern = require(\"matches\").pattern;"]).
intermezzo(Forms) -> [ compile(F) || F <- Forms ].
coda() -> io_lib:format("~s~n",["start();"]).

main() ->
    Forms = forms("fac.erl"),
    io:format("Forms: ~p",[Forms]),
    io:format("~s",[lists:flatten([prelude(),intermezzo(Forms),coda()])]).
Получаем на выходе fac.js:
var pattern = require("matches").pattern;
var start = pattern({
        '': function() {
                j = 5;
                n = fac(j);
                return console.log('factorial ~p',[j,[n,[]]]);
        }});
var fac = pattern({
        '0': function(x1) {
                return 1;
        },
        'n': function(n) {
                return n * fac(n - 1);
        }});
start();
Запускаем node fac.js:
factorial ~p [ 5, [ 120, [] ] ]
Факториал посчитан :) https://github.com/5HT/shen
Tags: cs, erlang, javascript
Subscribe
  • Post a new comment

    Error

    default userpic

    Your IP address will be recorded 

  • 42 comments