Elixir Under Microscope

26/03/2015

Когда я только начинал изучать Elixir мне было интересно посмотреть на то, что он генерирует, что в итоге попадает в BEAM. Спрашивал в официальном IRC канале – мне сказали что это невозможно.

Но никто мне тогда не сказал, что BEAM файлы можно декомпилировать, если они собраны с флагом debug_info. Чтобы дальше было проще:

#!/usr/bin/env escript
 
main([BeamFile]) ->
    {ok, {_, [{abstract_code, {_, AC}}]}} = beam_lib:chunks(BeamFile, [abstract_code]),
    io:fwrite("~s~n", [erl_prettypr:format(erl_syntax:form_list(AC))]).

Утилита в качестве аргумента принимает путь до BEAM файла и печатает на экран Erlang.

Эта запись будет скорее выжимкой, ни на что не претендующей, я ради интереса посмотрел какой код генерирует Эликсир и решил поделиться открытиями. Некоторые вещи я решил намеренно опустить, т.к. в них я не нашел ничего интересного, например – спеки функций. А еще утилита не всегда выводит отформатированный код, поэтому я причесал его ручками для этой записи.

Примерно вот так выглядит Эликсировский модуль с функией add/2:

-compile(no_auto_import).
-file("lib/eum.ex", 1).
-module('Elixir.Eum').
-export(['__info__'/1, add/2]).

'__info__'(functions) -> [{add, 2}];
'__info__'(macros) -> [];
'__info__'(module) -> 'Elixir.Eum';
'__info__'(atom) -> module_info(atom).

add(a@1, b@1) -> a@1 + b@1.

Строки действительно являются бинарниками, поэтому с ними в Эликсире жить намного проще:

# elixir
def string do
  "hellö"
end

def binary do
  <<"hellö">>
end
% erlang
string() -> <<"hellö">>.
binary() -> <<"hellö">>.

Очень “интересно” сделали с интерполяцией:

# elixir
def interpolation_simple do
  "hellö #{:world}"
end

def interpolation_complex do
  "hellö #{'complex'} #{:world} #{42}"
end
% erlang
interpolation_simple() ->
    <<"hellö ",
        case world of
            _rewrite@1 when erlang:is_binary(_rewrite@1) ->
                _rewrite@1;
            _rewrite@2 ->
                'Elixir.String.Chars':to_string(_rewrite@2)
        end/binary>>.

interpolation_complex() ->
    <<"hellö ",
        case [99, 111, 109, 112, 108, 101, 120] of
            _rewrite@1 when erlang:is_binary(_rewrite@1) ->
                _rewrite@1;
            _rewrite@2 ->
                'Elixir.String.Chars':to_string(_rewrite@2)
        end/binary,
    " ",
        case world of
            _rewrite@3 when erlang:is_binary(_rewrite@3) ->
                _rewrite@3;
            _rewrite@4 ->
                'Elixir.String.Chars':to_string(_rewrite@4)
        end/binary,
    " ",
        case 42 of
            _rewrite@5 when erlang:is_binary(_rewrite@5) ->
                _rewrite@5;
            _rewrite@6 ->
                'Elixir.String.Chars':to_string(_rewrite@6)
        end/binary>>.

В эликсире есть оператор cond, который немного сокращает код:

# elixir
def cond1 do
  cond do
    2 + 2 == 5 ->
      "This will not be true"
    2 * 2 == 3 ->
      "Nor this"
    1 + 1 == 2 ->
      "But this will"
  end
end

def cond2 do
  cond do
    hd([1,2,3]) ->
      "1 is considered as true"
  end
end
% erlang
cond1() ->
    case 2 + 2 == 5 of
        true -> <<"This will not be true">>;
        false ->
            case 2 * 2 == 3 of
                true -> <<"Nor this">>;
                false ->
                    case 1 + 1 == 2 of
                        true -> <<"But this will">>;
                        false -> erlang:error(cond_clause)
                    end
            end
    end.

cond2() ->
    case erlang:hd([1, 2, 3]) of
        _@1 when _@1 /= nil andalso _@1 /= false ->
            <<"1 is considered as true">>;
        _ -> erlang:error(cond_clause)
    end.

Сахарок для proplists:

# elixir
def proplist do
  pl = [{:a, 1}, {:b, 2}]
  _a = pl[:a]

  pl = [a: 1, b: 2]
  _a = pl[:a]
end
% erlang
proplist() ->
    pl@1 = [{a, 1}, {b, 2}],
    _a@1 = 'Elixir.Access':get(pl@1, a),
    pl@2 = [{a, 1}, {b, 2}],
    _a@2 = 'Elixir.Access':get(pl@2, a).

Ключевое слово def создает функцию, defp тоже создает функцию, но не экспортирует ее, а если приватная функция не используется в модуле, то она не попадает в beam:

# elixir
defmodule Eum
  def make_private_exist do
    private_fun_exist()
  end

  defp private_fun_exist do
    :private
  end

  defp private_fun_nonexist do
    :private
  end
end
% erlang
-module('Elixir.Eum').
-export(['__info__'/1, make_private_exist/0]).

'__info__'(functions) -> [{make_private_exist, 0}];
'__info__'(macros) -> [];
'__info__'(module) -> 'Elixir.Eum';
'__info__'(atom) -> module_info(atom).

make_private_exist() -> private_fun_exist().
private_fun_exist() -> private.

Функция с аргументами по умолчанию создает несколько Erlang функций для каждой арности:

# elixir
def default_args(default \\ "asd") do
  default
end

def complex_default(a \\ 1, b \\ 2, c \\ 3) do
  a + b + c
end
% erlang
complex_default() ->
    complex_default(1, 2, 3).
complex_default(x0@1) ->
    complex_default(x0@1, 2, 3).
complex_default(x0@1, x1@1) ->
    complex_default(x0@1, x1@1, 3).
complex_default(a@1, b@1, c@1) ->
    a@1 + b@1 + c@1.


default_args() ->
    default_args(<<"asd">>).
default_args(default@1) ->
    default@1.

Структуры тоже сделали интересно:

# elixir
defmodule UserStruct do
  defstruct name: "John", age: 27

  def new(name \\ "John Doe") do
    %UserStruct{name: name}
  end

  def update do
    john = %UserStruct{}
    mari = %{john | name: "Mari"}

    %UserStruct{name: name} = john
  end
end
% erlang
-module('Elixir.UserStruct').
-export(['__info__'/1, '__struct__'/0, new/0, new/1,
   update/0]).

'__info__'(functions) ->
    [{'__struct__', 0}, {new, 0}, {new, 1}, {update, 0}];
'__info__'(macros) -> [];
'__info__'(module) -> 'Elixir.UserStruct';
'__info__'(atom) -> module_info(atom).

new() -> new(<<"John Doe">>).

new(name@1) ->
    #{'__struct__' => 'Elixir.UserStruct', age => 27, name => name@1,
      '__struct__' => 'Elixir.UserStruct'}.

update() ->
    john@1 = #{'__struct__' => 'Elixir.UserStruct',
         age => 27, name => <<"John">>,
         '__struct__' => 'Elixir.UserStruct'},
    mari@1 = john@1#{name := <<"Mari">>},
    #{name := name@1, '__struct__' := 'Elixir.UserStruct'} = john@1.

'__struct__'() ->
    #{'__struct__' => 'Elixir.UserStruct', age => 27, name => <<"John">>}.

List comprehensions:

# elixir
def comprehension do
  _a = for n <- [1, 2, 3, 4], do: n * n

  dirs = []
  _b = for dir <- dirs, file <- File.ls!(dir), path = Path.join(dir, file), File.regular?(path) do
    File.rm!(path)
  end
end
% erlang
comprehension() ->
    _a@1 = lists:reverse('Elixir.Enum':reduce([1, 2, 3, 4], [],
        fun (n@1, _@1) ->
          [n@1 * n@1 | _@1]
        end)),
    dirs@1 = [],
    _b@1 = lists:reverse('Elixir.Enum':reduce(dirs@1, [],
        fun (dir@1, _@3) ->
            'Elixir.Enum':reduce('Elixir.File':'ls!'(dir@1), _@3,
                fun(file@1, _@3) ->
                    case path@1 = 'Elixir.Path':join(dir@1, file@1) of
                       _@5 when _@5 == false orelse _@5 == nil ->
                           _@3;
                       _ ->
                          case 'Elixir.File':'regular?'(path@1) of
                              _@6 when _@6 == false orelse _@6 == nil ->
                                  _@3;
                              _ ->
                                  ['Elixir.File':'rm!'(path@1) | _@3]
                          end
                    end
                end)
        end)).

Протоколы

# elixir
defmodule Eum
  defprotocol Blank do
    def blank?(data)
  end

  defimpl Blank, for: List do
    def blank?([]), do: true
    def blank?(_),  do: false
  end

  def test_proto do
    Blank.blank?([])
    Blank.blank?([1, 2, 3])
  end
end
% erlang
% Elixir.Eum.Blank.beam
-module('Elixir.Eum.Blank').
-compile(debug_info).
-compile({inline,
    [{any_impl_for, 0}, {struct_impl_for, 1},
     {'impl_for?', 1}]}).
-protocol([{fallback_to_any, false},
     {consolidated, false}]).
-export_type([{t, 0}]).
-type t() :: term().
-callback 'blank?'(t()) -> term().

-export(['__info__'/1, '__protocol__'/1, 'blank?'/1,
   impl_for/1, 'impl_for!'/1]).

'__info__'(functions) ->
    [{'__protocol__', 1}, {'blank?', 1}, {impl_for, 1},
     {'impl_for!', 1}];
'__info__'(macros) -> [];
'__info__'(module) -> 'Elixir.Eum.Blank';
'__info__'(atom) -> module_info(atom).

impl_for(#{'__struct__' := _@1}) when erlang:is_atom(_@1) ->
    struct_impl_for(_@1);
impl_for(_@1) when erlang:is_tuple(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Tuple') of
        true -> 'Elixir.Eum.Blank.Tuple':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_atom(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Atom') of
        true -> 'Elixir.Eum.Blank.Atom':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_list(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.List') of
        true -> 'Elixir.Eum.Blank.List':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_map(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Map') of
        true -> 'Elixir.Eum.Blank.Map':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_bitstring(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.BitString') of
        true -> 'Elixir.Eum.Blank.BitString':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_integer(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Integer') of
        true -> 'Elixir.Eum.Blank.Integer':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_float(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Float') of
        true -> 'Elixir.Eum.Blank.Float':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_function(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Function') of
        true -> 'Elixir.Eum.Blank.Function':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_pid(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.PID') of
        true -> 'Elixir.Eum.Blank.PID':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_port(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Port') of
        true -> 'Elixir.Eum.Blank.Port':'__impl__'(target);
        false -> any_impl_for()
    end;
impl_for(_@1) when erlang:is_reference(_@1) ->
    case 'impl_for?'('Elixir.Eum.Blank.Reference') of
        true -> 'Elixir.Eum.Blank.Reference':'__impl__'(target);
        false -> any_impl_for()
    end.

'impl_for!'(_@1) ->
    case impl_for(_@1) of
        _@2 when _@2 =:= nil orelse _@2 =:= false ->
            erlang:error('Elixir.Protocol.UndefinedError':exception([{protocol,
                'Elixir.Eum.Blank'}, {value, _@1}]));
        _@3 -> _@3
    end.

struct_impl_for(_@1) ->
    _@2 = 'Elixir.Module':concat('Elixir.Eum.Blank', _@1),
    case 'impl_for?'(_@2) of
        true -> _@2:'__impl__'(target);
        false -> any_impl_for()
    end.

'impl_for?'(_@1) ->
    'Elixir.Code':'ensure_compiled?'(_@1) andalso
      'Elixir.Kernel':'function_exported?'(_@1, '__impl__', 1).

'__protocol__'(name) -> 'Elixir.Eum.Blank';
'__protocol__'(functions) -> [{'blank?', 1}].

any_impl_for() -> nil.

'blank?'(_@1) -> ('impl_for!'(_@1)):'blank?'(_@1).

% Elixir.Eum.Blank.List.beam
-module('Elixir.Eum.Blank.List').
-impl([{protocol, 'Elixir.Eum.Blank'},
       {for, 'Elixir.List'}]).
-behaviour('Elixir.Eum.Blank').
-export(['__impl__'/1, '__info__'/1, 'blank?'/1]).

'__info__'(functions) ->
    [{'__impl__', 1}, {'blank?', 1}];
'__info__'(macros) -> [];
'__info__'(module) -> 'Elixir.Eum.Blank.List';
'__info__'(atom) -> module_info(atom).

'__impl__'(for) -> 'Elixir.List';
'__impl__'(target) -> 'Elixir.Eum.Blank.List';
'__impl__'(protocol) -> 'Elixir.Eum.Blank'.

'blank?'([]) -> true;
'blank?'(_) -> false.

На этом, пожалуй, все. Надо будет еще на макросы посмотреть.