web-dev-qa-db-ja.com

ElixirまたはErlangで実行時にモジュールを動的に作成およびロードするにはどうすればよいですか?

基本的なシナリオは次のとおりです。データベースからテキストをロードし、そのテキストをElixirモジュール(またはErlangモジュール)に変換してから、呼び出しを行う必要があります。テキストは実質的にモジュールファイルと同じです。つまり、これはホットコードの読み込みの一形態です。 「ファイル」をコンパイルしてから、結果のモジュールをロードしてから、そのモジュールを呼び出したいと思います。後でアンロードします。唯一の違いは、コードがディスク上のファイルではなくデータベースに存在することです。 (そして、それをロードするコードを書いている時点では存在しません。)

Erlangがホットコードロードをサポートしていることは知っていますが、ディスク上のファイルをコンパイルしてからビームをロードすることに重点を置いているようです。これをより動的なプロセスとして実行したいので、実行中のコードを置き換えるのではなく、コードをロードしてから実行してからアンロードします。

Elixirには、実行時にコードを評価するための機能がいくつかあります。私はそれらを使ってこれを行う方法を理解しようとしていますが、ドキュメントは少しまばらです。

Code.compile_string(string, "nofile")

「最初の要素がモジュール名で、2番目の要素がそのバイナリであるタプルのリストを返します」。これで、モジュール名とそのバイナリができましたが、バイナリをランタイムにロードして呼び出す方法がわかりません。どうすればいいですか? (私が見ることができるコードライブラリにはそのための関数はありません。)

おそらく、Erlang関数を使用できます。

:code.load_binary(Module, Filename, Binary)  ->
           {module, Module} | {error, What}

さて、これはatom "module"、次にModuleのタプルを返します。データベースからロードされた文字列が "Paris"というモジュールを定義した場合、コードでどのように実行しますか?

paris.handler([parameters])

パリというモジュールがあることを事前に知らないので?文字列「paris」もデータベースに保存されているので、これが名前であることがわかりますが、呼び出しているモジュールの名前として文字列を使用して、モジュールを呼び出す方法はありますか?

もあります:

eval(string, binding // [], opts // [])

文字列の内容を評価します。この文字列をモジュールの定義全体にすることはできますか?そうではないようです。相互に呼び出す複数の関数を持つように評価されているこのコードを記述できるようにしたいと思います。事前定義されたエントリポイントを備えた完全な小さなプログラム(「DynamicModule.handle([parameter、list])」などのメインになる可能性があります)

次に、EExモジュールがあります。これには次のものがあります。

compile_string(source, options // [])

これはテンプレートを作成するのに最適です。しかし、最終的には、文字列があり、Elixirコードが埋め込まれているユースケースでのみ機能するようです。オプションのコンテキストで文字列を評価し、文字列を生成します。文字列をコンパイルして、呼び出し可能な1つ以上の関数にしようとしています。 (問題のない関数を1つしか作成できない場合、その関数はパターンマッチングを行うか、必要な他の処理に切り替えることができます。..)

私はこれが型破りであることを知っています、しかし私はそれをこのようにする理由があり、それらは良いものです。これを行う方法についてのアドバイスを探していますが、「それをしないでください」と言われる必要はありません。可能であるように思われます。Erlangはホットコードのロードをサポートし、Elixirはかなり動的ですが、構文や適切な関数がわかりません。私はこの質問を注意深く監視します。前もって感謝します!


最初の回答に基づいて編集:

答えてくれてありがとう、これは良い進歩です。 Yuriが示したように、evalはモジュールを定義でき、Joséが指摘しているように、バインディングのあるコードの小さな部分にコードevalを使用できます。

評価されるコード(モジュールに変換されるかどうかに関係なく)はかなり複雑になります。そして、その開発は、それを関数に分解し、それらの関数を呼び出すことを含むのが最善でしょう。

助けるために、私はいくつかの文脈を提供させてください。 Webフレームワークを構築していると仮定します。データベースから読み込まれるコードは、特定のURIのハンドラーです。したがって、HTTP呼び出しが着信すると、example.com/blog /のコードをロードする可能性があります。このページには、コメント、最近の投稿のリストなど、いくつかの異なるものが含まれる場合があります。

多くの人が同時にページにアクセスしているので、各ページビューを処理するプロセスを生成しています。したがって、さまざまな要求に対して、このコードが同時に評価されることがよくあります。

モジュールソリューションを使用すると、コードをページのさまざまな部分の関数に分割できます(例:投稿のリスト、コメントなど)。起動時にモジュールを1回ロードし、多くのプロセスがその呼び出しを生成するようにします。それに。モジュールはグローバルですよね?

モジュールがすでに定義されている場合はどうなりますか? EG:モジュールが変更され、そのモジュールをすでに呼び出しているプロセスがある場合。

Iexでは、すでに定義されているモジュールを再定義できます。

iex(20)> Code.eval "defmodule A do\ndef a do\n5\nend\nend"
nofile:1: redefining module A

実行時にモジュールを再定義すると、そのモジュールを現在呼び出しているすべてのプロセスに対してどうなりますか?また、この再定義は、通常の操作でiexの外部で機能しますか?

モジュールの再定義に問題があり、グローバルなモジュールで名前空間の衝突の問題が発生する可能性があると想定して、evalを使用して関数を定義することを検討しました。

データベースのコードに関数を定義させることができれば、それらの関数はどのプロセスの範囲内にもあり、グローバルな衝突の可能性はありません。

ただし、これは機能していないようです。

iex(31)> q = "f = function do
...(31)> x, y when x > 0 -> x+y
...(31)> x, y -> x* y
...(31)> end"
"f = function do\nx, y when x > 0 -> x+y\nx, y -> x* y\nend"
iex(32)> Code.eval q
{#Fun<erl_eval.12.82930912>,[f: #Fun<erl_eval.12.82930912>]}
iex(33)> f
** (UndefinedFunctionError) undefined function: IEx.Helpers.f/0
    IEx.Helpers.f()
    erl_eval.erl:572: :erl_eval.do_apply/6
    src/elixir.erl:110: :elixir.eval_forms/3
    /Users/jose/Work/github/elixir/lib/iex/lib/iex/loop.ex:18: IEx.Loop.do_loop/1

iex(33)> f.(1,3)
** (UndefinedFunctionError) undefined function: IEx.Helpers.f/0
    IEx.Helpers.f()
    erl_eval.erl:572: :erl_eval.do_apply/6
    erl_eval.erl:355: :erl_eval.expr/5
    src/elixir.erl:110: :elixir.eval_forms/3
    /Users/jose/Work/github/elixir/lib/iex/lib/iex/loop.ex:18: IEx.Loop.do_loop/1

私も試しました:

    iex(17)> y = Code.eval "fn(a,b) -> a + b end"
{#Fun<erl_eval.12.82930912>,[]}
iex(18)> y.(1,2)
** (BadFunctionError) bad function: {#Fun<erl_eval.12.82930912>,[]}
    erl_eval.erl:559: :erl_eval.do_apply/5
    src/elixir.erl:110: :elixir.eval_forms/3
    /Users/jose/Work/github/elixir/lib/iex/lib/iex/loop.ex:18: IEx.Loop.do_loop/1

したがって、要約すると:

  1. モジュールを呼び出すプロセスがある場合、Code.evalを使用してモジュールを再定義できますか?

  2. Code.evalを使用して、スコープがCode.evalが呼び出されたプロセスにバインドされている関数を作成することは可能ですか?

  3. 私がやろうとしていることを理解しているなら、それについてもっと良い方法を提案できますか?

また、私がこれを尋ねるべきであるより良いフォーラムがあれば、私に知らせてください。また、読むべきドキュメントや関連する例がある場合は、遠慮なくそれらを指摘してください。私はあなたにすべての仕事をさせようとしているのではなく、自分でそれを理解することができないだけです。

私は特にコードを動的に評価する機能についてElixirを学んでいますが、Elixirの知識は今では最小限であり、始めたばかりです。また、erlangも錆びています。

どうもありがとう!

35
nirvana

あなたが説明したように、最終的には2つの異なるカテゴリに要約することによって取ることができる多くの異なるアプローチがあります:1)コードコンパイルと2)コード評価。上記の例では、モジュールを定義するコンパイルが必要であり、それを呼び出す必要があります。ただし、ご存知のとおり、モジュール名を定義し、データベースが変更されたときにそれらのモジュールをパージおよび破棄する可能性があります。また、モジュールを定義すると、atomテーブルが使い果たされる可能性があることに注意してください。これは、atomがすべてのモジュールに対して作成されるためです。このアプローチは、必要な場合にのみ使用します。最大で12個のモジュールをコンパイルします。

(小さなメモ、Code.compile_stringはすでにモジュールを定義しているので、load_binaryを呼び出す必要はありません)。

おそらく、より簡単なアプローチは、Code.evalを呼び出してデータベースからコードを渡し、コード評価を行うことです。いくつかのカスタムルールを評価するだけであれば、問題なく機能します。 Code.evalはバインディングを受け入れます。これにより、パラメーターをコードに渡すことができます。データベースに「a + b」が格納されていると仮定します。ここで、abはパラメータであり、次のように評価できます。

Code.eval "a + b", [a: 1, b: 1]

質問の編集に基づいて編集

コードを評価する場合は、Code.evalがコードと新しいバインディングを評価した結果を返すため、上記の例は次のように記述した方がよいことに注意してください。

q = "f = function do
x, y when x > 0 -> x+y
x, y -> x* y
end"

{ _, binding } = Code.eval q
binding[:f].(1, 2)

ただし、ユースケースを考えると、evalの使用は検討しませんが、実際にモジュールをコンパイルします。情報はデータベースから取得されるため、このファクトを使用して、レコードごとに一意のモジュールを生成できます。たとえば、開発者がモジュールのコンテンツをデータベースに追加すると想定できます。たとえば、次のようになります。

def foo, do: 1
def bar, do: 1

データベースからこの情報を取得したら、次を使用してコンパイルできます。

record   = get_record_from_database
id       = record.id
contents = Code.string_to_quoted!(record.body)
module   = Module.concat(FromDB, "Data#{record.id}")
Module.create module, contents, Macro.Env.location(__ENV__)
module

レコードとは、データベースから取得したものです。レコードIDが1であるとすると、モジュールFromDB.Data1が定義され、foobarを呼び出すことができます。

コードのリロードに関しては、defmoduleModule.createの両方が:code.load_binaryを使用してモジュールをロードします。つまり、モジュールを更新しても、別の新しいバージョンがロードされるまで、古いバージョンが保持されます。

同様に追加する必要があるものの1つはキャッシュの有効期限であるため、要求ごとにモジュールをコンパイルする必要はありません。これは、レコードの内容を変更するたびにインクリメントするlock_versionがデータベースにある場合に実行できます。最終的なコードは次のようになります。

record  = get_record_from_database
module  = Module.concat(FromDB, "Data#{record.id}")
compile = :code.is_loaded(module) == false or
            record.lock_version > module.lock_version

if compile do
  id       = record.id
  contents = Code.string_to_quoted!(record.body)
  contents = quote do
    def lock_version, do: unquote(record.lock_version)
    unquote(contents)
  end
  Module.create module, contents, Macro.Env.location(__ENV__)
end

module
33
José Valim

Code.evalを使用して、モジュールを定義できます。

iex(1)> Code.eval "defmodule A do\ndef a do\n1\nend\nend" 
{{:module,A,<<70,79,82,49,0,0,2,168,66,69,65,77,65,116,111,109,0,0,0,98,0,0,0,11,8,69,108,105,120,105,114,45,65,8,95,95,105,110,102,111,95,95,4,100,111,99,115,9,102,117,...>>,{:a,0}},[]}
iex(2)> A.a
1

これは役に立ちますか?

11