Elixirでのプロトコルの仕組み

当社では、Erlangを積極的に使用していますが、多くの場合、他の代替言語やアプローチを考慮して、独自のコードの品質を向上させています。







Elixirは、BeamVM仮想マシンで実行される機能的な汎用プログラミング言語です。 Erlangとは、Rubyにより類似した構文と、高度なメタプログラミング機能が異なります。







ElixirにはProtocolsと呼ばれるポリモーフィズムのための優れたメカニズムもありますが、Erlangにはそれらを実装するために必要な動的ディスパッチの構文がありません。







次に、それらはどのように内部に配置されますか? プロトコルを使用してコードに与えるオーバーヘッドはどれくらいですか? それを理解してみましょう。













内部で何が起こっているかを理解するには、2つの方法があります。







-Elixir CompilerがBeamVMのコードを生成する方法を理解するには、

-ビームファイルを逆コンパイルし、最終的に何が起こったかを確認します。







2番目の方法ははるかに簡単で、使用します。







まず、新しいプロジェクトを作成します。







mix new proto cd proto
      
      





次に、かなり単純な例としてlib/proto.ex



を編集しlib/proto.ex









 defprotocol Double do def double(input) end defimpl Double, for: Integer do def double(int) do int * 2 end end defimpl Double, for: List do def double(list) do list ++ list end end
      
      





ここで、 double/1



インターフェイスと、 Integer



およびList



このプロトコルの2つの実装を備えた新しいDouble



プロトコルを発表しました。







パフォーマンスを確認します。







 iex(1)> Double.double(2) 4 iex(2)> Double.double([1,2,3]) [1, 2, 3, 1, 2, 3] iex(3)> Double.double(:atom) ** (Protocol.UndefinedError) protocol Double not implemented for :atom (proto) lib/proto.ex:1: Double.impl_for!/1 (proto) lib/proto.ex:2: Double.double/1
      
      





次に、コンパイルされたファイルの構造を見てみましょう。







 $ tree _build/dev/ _build/dev/ ├── consolidated │ ├── Elixir.Collectable.beam │ ├── Elixir.Double.beam │ ├── Elixir.Enumerable.beam │ ├── Elixir.IEx.Info.beam │ ├── Elixir.Inspect.beam │ ├── Elixir.List.Chars.beam │ └── Elixir.String.Chars.beam └── lib └── proto └── ebin ├── Elixir.Double.beam ├── Elixir.Double.Integer.beam ├── Elixir.Double.List.beam └── proto.app
      
      





最初に目を引くのは、統合ディレクトリとlib / proto / ebinディレクトリに同じ名前のモジュールが存在することです。 それらの内容を考慮してください。







まず、ビームファイルを逆コンパイルする必要があります。 これを行うには、 beam_to_erl



ファイルbeam_to_erl



作成します







 #!/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))]).
      
      





すべてのビームファイルを実行します。







 $ for f in $(find _build/ -name "*.beam"); do ./beam_to_erl $f > "${f%.beam}.erl"; done
      
      





 $ tree _build/dev/ | grep -v ".beam" _build/dev/ ├── consolidated │ ├── Elixir.Collectable.erl │ ├── Elixir.Double.erl │ ├── Elixir.Enumerable.erl │ ├── Elixir.IEx.Info.erl │ ├── Elixir.Inspect.erl │ ├── Elixir.List.Chars.erl │ └── Elixir.String.Chars.erl └── lib └── proto └── ebin ├── Elixir.Double.erl ├── Elixir.Double.Integer.erl ├── Elixir.Double.List.erl └── proto.app
      
      





ファイルlib/proto/ebin/Elixir.Double.erl



の内容を考慮してください。







 -compile(no_auto_import). -file("lib/proto.ex", 1). -module('Elixir.Double'). -compile(debug_info). -compile({inline, [{any_impl_for, 0}, {struct_impl_for, 1}, {'impl_for?', 1}]}). -protocol([{fallback_to_any, false}]). -export_type([t/0]). -type t() :: term(). -spec '__protocol__'('consolidated?') -> boolean(); (functions) -> [{double, 1}, ...]; (module) -> 'Elixir.Double'. -spec impl_for(term()) -> atom() | nil. -spec 'impl_for!'(term()) -> atom() | no_return(). -callback double(t()) -> term(). -export(['__info__'/1, '__protocol__'/1, double/1, impl_for/1, 'impl_for!'/1]). -spec '__info__'(attributes | compile | exports | functions | macros | md5 | module | native_addresses) -> atom() | [{atom(), any()} | {atom(), byte(), integer()}]. '__info__'(functions) -> [{'__protocol__', 1}, {double, 1}, {impl_for, 1}, {'impl_for!', 1}]; '__info__'(macros) -> []; '__info__'(info) -> erlang:get_module_info('Elixir.Double', info). '__protocol__'(module) -> 'Elixir.Double'; '__protocol__'(functions) -> [{double, 1}]; '__protocol__'('consolidated?') -> false. any_impl_for() -> nil. double(_@1) -> ('impl_for!'(_@1)):double(_@1). 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.Double.Tuple') of true -> 'Elixir.Double.Tuple':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_atom(_@1) -> case 'impl_for?'('Elixir.Double.Atom') of true -> 'Elixir.Double.Atom':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_list(_@1) -> case 'impl_for?'('Elixir.Double.List') of true -> 'Elixir.Double.List':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_map(_@1) -> case 'impl_for?'('Elixir.Double.Map') of true -> 'Elixir.Double.Map':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_bitstring(_@1) -> case 'impl_for?'('Elixir.Double.BitString') of true -> 'Elixir.Double.BitString':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_integer(_@1) -> case 'impl_for?'('Elixir.Double.Integer') of true -> 'Elixir.Double.Integer':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_float(_@1) -> case 'impl_for?'('Elixir.Double.Float') of true -> 'Elixir.Double.Float':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_function(_@1) -> case 'impl_for?'('Elixir.Double.Function') of true -> 'Elixir.Double.Function':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_pid(_@1) -> case 'impl_for?'('Elixir.Double.PID') of true -> 'Elixir.Double.PID':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_port(_@1) -> case 'impl_for?'('Elixir.Double.Port') of true -> 'Elixir.Double.Port':'__impl__'(target); false -> any_impl_for() end; impl_for(_@1) when erlang:is_reference(_@1) -> case 'impl_for?'('Elixir.Double.Reference') of true -> 'Elixir.Double.Reference':'__impl__'(target); false -> any_impl_for() end; impl_for(_) -> any_impl_for(). 'impl_for!'(_@1) -> case impl_for(_@1) of _@2 when (_@2 =:= nil) or (_@2 =:= false) -> erlang:error('Elixir.Protocol.UndefinedError':exception([{protocol, 'Elixir.Double'}, {value, _@1}])); _@3 -> _@3 end. 'impl_for?'(_@1) -> case 'Elixir.Code':'ensure_compiled?'(_@1) of true -> 'Elixir.Kernel':'function_exported?'(_@1, '__impl__', 1); false -> false; _@2 -> erlang:error({badbool, 'and', _@2}) end. struct_impl_for(_@1) -> _@2 = 'Elixir.Module':concat('Elixir.Double', _@1), case 'impl_for?'(_@2) of true -> _@2:'__impl__'(target); false -> any_impl_for() end.
      
      





そして、それはすべて魔法です。 double/1



関数を見てみましょう。







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





彼女はimpl_for/1



を介して渡された引数に適したモジュールを検索し、その実装を呼び出します。







そして、引数のモジュールを見つける方法は? 非常にシンプル:







-プリミティブ型またはbif型の場合、「Elixir。{ProtocolName}。{TypeName}」という名前のモジュールを探しています。ここで、ProtocolNameはプロトコル名、TypeNameは型名です。 まだロードされていない場合は、 'Elixir.Code':'ensure_compiled?'/1



。 関数'__impl__'/1



の存在により、モジュールがプロトコルの実装であるかどうかを確認し、実装モジュール'__impl__'(target)



を取得します。

-これが構造体の場合は、 __struct__



サービスフィールドを見て、同じように「Elixir。{ProtocolName}。{StructName}」モジュールを探します。

-実装が見つからない場合、任意のタイプのデフォルト実装を確認するか、エラーを返します。







プロトコルの実装はほとんど変更されていません。 いくつかのシステム機能のみが追加されます。 例: 'Elixir.Double.Integer'









 -compile(no_auto_import). -file("lib/proto.ex", 5). -module('Elixir.Double.Integer'). -behaviour('Elixir.Double'). -impl([{protocol, 'Elixir.Double'}, {for, 'Elixir.Integer'}]). -spec '__impl__'(protocol) -> 'Elixir.Double'; (target) -> 'Elixir.Double.Integer'; (for) -> 'Elixir.Integer'. -export(['__impl__'/1, '__info__'/1, double/1]). -spec '__info__'(attributes | compile | exports | functions | macros | md5 | module | native_addresses) -> atom() | [{atom(), any()} | {atom(), byte(), integer()}]. '__info__'(functions) -> [{'__impl__', 1}, {double, 1}]; '__info__'(macros) -> []; '__info__'(info) -> erlang:get_module_info('Elixir.Double.Integer', info). '__impl__'(for) -> 'Elixir.Integer'; '__impl__'(target) -> 'Elixir.Double.Integer'; '__impl__'(protocol) -> 'Elixir.Double'. double(int@1) -> int@1 * 2.
      
      





つまり、すべての動的ディスパッチは、プロトコルを実装するためにこの名前をコンパイルするアルゴリズムを知っているため、名前でモジュールを検索することになります。 このアプローチには重要でないマイナスが1つあります。同じタイプに対して複数のプロトコル実装を定義することはできません。







同時に、特に負荷の高いシステムでは、オーバーヘッドはそれほど小さくありません。 ポイントは、実行時にモジュールの存在を常にチェックすることです。







この欠点を解消するため、コンパイル段階で既知のプロトコル実装のルーティングを「縫い付ける」機能がディスパッチ関数impl_for/1



直接追加されました

このコンパイラー関数は統合プロトコルと呼ば 、Elixir v1.2では、リリースビルドのミックス中に自動的に実装されます。







consolidated/Elixir.Double.erl









 -compile(no_auto_import). -file("lib/proto.ex", 1). -module('Elixir.Double'). -compile(debug_info). -compile({inline, [{any_impl_for, 0}, {struct_impl_for, 1}, {'impl_for?', 1}]}). -protocol([{fallback_to_any, false}]). -export_type([t/0]). -type t() :: term(). -spec '__protocol__'('consolidated?') -> boolean(); (functions) -> [{double, 1}, ...]; (module) -> 'Elixir.Double'. -spec impl_for(term()) -> atom() | nil. -spec 'impl_for!'(term()) -> atom() | no_return(). -callback double(t()) -> term(). -export(['__info__'/1, '__protocol__'/1, double/1, impl_for/1, 'impl_for!'/1]). -spec '__info__'(attributes | compile | exports | functions | macros | md5 | module | native_addresses) -> atom() | [{atom(), any()} | {atom(), byte(), integer()}]. '__info__'(functions) -> [{'__protocol__', 1}, {double, 1}, {impl_for, 1}, {'impl_for!', 1}]; '__info__'(macros) -> []; '__info__'(info) -> erlang:get_module_info('Elixir.Double', info). '__protocol__'(module) -> 'Elixir.Double'; '__protocol__'(functions) -> [{double, 1}]; '__protocol__'('consolidated?') -> true. any_impl_for() -> nil. double(_@1) -> ('impl_for!'(_@1)):double(_@1). impl_for(#{'__struct__' := x}) when erlang:is_atom(x) -> struct_impl_for(x); impl_for(x) when erlang:is_list(x) -> 'Elixir.Double.List'; impl_for(x) when erlang:is_integer(x) -> 'Elixir.Double.Integer'; impl_for(_) -> nil. 'impl_for!'(_@1) -> case impl_for(_@1) of _@2 when (_@2 =:= nil) or (_@2 =:= false) -> erlang:error('Elixir.Protocol.UndefinedError':exception([{protocol, 'Elixir.Double'}, {value, _@1}])); _@3 -> _@3 end. 'impl_for?'(_@1) -> case 'Elixir.Code':'ensure_compiled?'(_@1) of true -> 'Elixir.Kernel':'function_exported?'(_@1, '__impl__', 1); false -> false; _@2 -> erlang:error({badbool, 'and', _@2}) end. struct_impl_for(_) -> nil.
      
      





モジュールコードは元のコードよりも大幅に小さく、重要なことに、 impl_for



はモジュールをチェックせずに1ステップでimpl_for



れます。







合計



ツールの動作を内側から見ると便利な場合があります。 これにより、その長所と短所をよりよく理解することができます。







プロトコルの実装は非常に単純であり、統合プロトコルを使用すると、オーバーヘッドがわずかになりますが、データ構造に対する優れた抽象化が提供されます。 それにもかかわらず、同様のメカニズムをErlangに簡単に追加できますが、これには動的ディスパッチ関数の手動記述が必要になります。







Elixirを使用するかどうかはあなた次第です。 しかし、私たちはまだアーランにとどまっています。








All Articles