パイセンの実装に感動
僕が感動したのはElixir
での実装ではありませんが、Ruby
で特定のディレクトリ以下に_____.rb
というファイルを作成し、命名規則に従ったモジュールとinterface
を満たす関数を実装すると、自動でモジュール内の関数が読み込まれて動的に実行されるという設計に感動しました。
このような設計の方法はデザインパターンに通ずることがあるんじゃないかと思いましたが、僕自身、オブジェクト指向をやんちゃにしか取り組んだことしかないので詳細は不明。
とはいえ、この実装方法に感動したのでElixir
を使って実装してみます。
Elixirでの実装方法
先に全体のコードを貼っておきます。
interface
Elixir
でinterface
を指定することは出来ませんが、behavior
を使用することで指定の関数を満たすモジュールを定義させることが出来るようです。
なるほど。たまに見かけてた@impl
ってこんな感じで使うんですね。
とはいえ、behavior
を使うのはちょっとリッチだなと思ったので、今回は共通認識として「この関数はモジュール内に実装必須ね」と開発メンバーに事前に共有をしたという前提で、通常のモジュールを用いて実装を行います。
モジュールの読み込み
以下の順で実装をしてみようと思います。
コードに落とすとこうなりました。思っていたよりも長くなってしまいました。staticな文字列にしたらもっと、短くなると思いますが自分のスタイルを貫きました。
def module_names do processes_path() # 指定のディレクトリのpathを文字列で取得 |> File.ls! # ディレクトリ内のファイル一覧を取得 |> Enum.filter(fn fname -> ex_extension_slice(fname) == ".ex" end) # .exファイル以外を除去 |> Enum.map(fn fname -> merge_module_name(fname) end) # この階層までのモジュール名を付与(eg: Project.Sample <> FileModule) end
これで指定モジュール内に格納されている.ex
ファイルを読み込む準備が出来ました。あとは読み込んだモジュールへのpathを良い感じに使ってあげます。今回は、モジュール内に共通で実装されているstart
という関数を実行させます(startは新たなプロセスを起動させるための関数)。
文字列から動的にモジュール内の関数を実行する方法は以下を参考にしました。ノリはspawn
でモジュール名と関数のアトムを渡すのに似ていますね。
dynamic - Elixir - Call method on module by String-name - Stack Overflowstackoverflow.com
ハマったのは新規で自分が作成したモジュールを読み込む場合にはElixir
をモジュール名に付与する必要があるということです。気付かなかった。
defp call_start_func(module_name, p_pid) do String.to_existing_atom("Elixir." <> module_name) # eg: Elixir. + Project.Sample.FileModule |> apply(:start, [p_pid]) end
モジュールに実装する動作について
これで指定ディレクトリ内のモジュールに実装されたstart
関数を実行することが出来る様になりました。
今回の場合にはlib/processes/children
に新たな.ex
ファイルを追加して、モジュール内にstartという関数を実行して、新たなプロセスを生成して、特定のプロセスにmessage passing
を行う(メッセージの送信)という実装にしてあります。
defmodule Processes.Children.Apple do def start(p_pid) do pid = spawn(fn -> run(p_pid) end) pid end defp run(p_pid) do # 特定のプロセスに「🍎」というメッセージを送信 Processes.Job.run(p_pid, "🍎") end end
モジュール名はProcesses.Children
以降は何でも良いです。分かりやすさ重視のため、絵文字を送っています。メッセージの送信処理はProcesses.Job
に共通化してありますが、以下のように新たに任意で作成することも当然可能です。
# original job defp run(p_pid) do IO.puts('hello world') run(p_pid) end
動作確認
メッセージを受け取る側のプロセスにloggerを仕込んであるので、実際にプロセスが動的に起動して、メッセージを送って来ているのかを確認してみます。
iex(1)> Main.execute
ログが流れ始めました。立ち上がったプロセス情報(PID
)を返すようにしています。
%{ children: [#PID<0.141.0>, #PID<0.142.0>, #PID<0.143.0>, #PID<0.144.0>, #PID<0.145.0>], parent: #PID<0.140.0> } iex(2)> 10:08:07.574 [info] 🐓⏰ => 2020/11/09 10:08:07 from #PID<0.145.0> as 🍍 10:08:08.698 [info] (′・ω・`) Received message from #PID<0.142.0> as 🍑 10:08:08.836 [info] (′・ω・`) Received message from #PID<0.141.0> as 🍊 10:08:09.789 [info] (′・ω・`) Received message from #PID<0.144.0> as 🍌 10:08:09.906 [info] (′・ω・`) Received message from #PID<0.143.0> as 🍎 10:08:10.041 [info] 🐓⏰ => 2020/11/09 10:08:10 from #PID<0.145.0> as 🍍 10:08:10.679 [info] (′・ω・`) Received message from #PID<0.141.0> as 🍊 : :
絵文字とモジュール名を連動させるようにしてあります。このログをみる限り以下のモジュールからメッセージを受けていることが分かります。
pineapple
からメッセージが送られて来た場合には現在時刻を表示するようにさせてあります。独自の処理を組み込むのも簡単だねってElixir
の宣伝です。