ヽ(´・肉・`)ノログ

How do we fight without fighting?

任意のBEAM系言語でプラグインを書ける安定したフレームワークの作りかた

任意のBEAM系言語でプラグインを書ける安定したフレームワークの作りかた

最初に

-

自己紹介

./icon.png

farmnote

./farmnote.png

サッポロビーム

./sapporobeam_poor.png

./sapporobeam_nice.png

何発表するの

なんで作ろうと思ったの

この発表を聞いてどうなってほしいの

Hobotを使ったアプリケーション例

https://github.com/niku/hobot#usage

hobot% iex -S mix
iex(1)> name = "EchoBot"
iex(2)> adapter = %{module: Hobot.Plugin.Adapter.Shell, args: [Process.group_leader()]}
iex(3)> handlers = [%{module: Hobot.Plugin.Handler.Echo, args: [["on_message"]]}]
iex(4)> {:ok, echo_bot} = Hobot.create(name, adapter, handlers)
iex(5)> %{adapter: adapter} = Hobot.context(echo_bot)
iex(6)> Hobot.Plugin.Adapter.Shell.gets("> ", Hobot.pid(adapter))
> hello
"hello"
> hi
"hi"
> quit
nil

Adapter

defmodule Hobot.Plugin.Adapter.Shell do
  use GenServer
  def gets(device \\ :stdio, prompt, send_to) do
    with x when is_binary(x) <- IO.gets(device, prompt),
         line when line !== "quit" <- String.trim_trailing(x) do
      send(send_to, line)
      gets(device, prompt, send_to)
    else
      _ ->
        nil
    end
  end
  def init({context, device}), do: {:ok, {context, device}}
  def handle_cast({:reply, _ref, data}, {_context, device} = state) do
    IO.puts(device, inspect(data))
    {:noreply, state}
  end
  def handle_info(data, {context, _device} = state) do
    apply(context.publish, ["on_message", make_ref(), data])
    {:noreply, state}
  end
end

Handler

defmodule Hobot.Plugin.Handler.Echo do
  use GenServer

  def init({context, topics} = args) do
    for topic <- topics, do: apply(context.subscribe, [topic])
    {:ok, args}
  end

  def handle_cast({:broadcast, _topic, ref, data}, {context, _topics} = state) do
    apply(context.reply, [ref, data])
    {:noreply, state}
  end

  def terminate(reason, {context, topics}) do
    for topic <- topics, do: apply(context.unsubscribe, [topic])
    reason
  end
end

フレームワーク作ってみてわかった

悩み

プラグインを読み込む

プラグインを読み込む

./module_load.png

フレームワークを起動すると特定のファイルパスを読み込みにいく

# my_framework.ex
defmodule MyFramework do
  @plugins_path "~/plugins"
  def load_plugins do
    for file <- File.ls!(Path.expand(@plugins_path)) do
      Code.load_file(Path.join(@plugins_path, file))
    end
  end
end
# ~/plugins/foo.exs
defmodule Foo do
  def greet, do: IO.puts("hello")
end
# ~/plugins/bar.exs
defmodule Bar do
  def greet, do: IO.puts("hi")
end
# iex
iex(1)> c("my_framework.ex")
iex(2)> MyFramework.load_plugins
iex(3)> Foo.greet # => hello
iex(4)> Bar.greet # => hi

mixを利用する

mix new my_framework
mix new foo
mix new bar
mix new framework_user
# foo/lib/foo.ex
defmodule Foo do
  def greet, do: IO.puts("hello")
end

# framework_user/mix.exs
  defp deps do
    [
      {:my_framework, path: "../my_framework"},
      {:foo, path: "../foo"},
      {:bar, path: "../bar"}
    ]
  end

# iex
cd framework_user
iex -S mix
Foo.greet # =>hello

フレームワークの登場人物

プラグインからフレームワークへの情報伝達

プラグインからフレームワークへの情報伝達

./plugin_to_framework.png

HobotプラグインはGenServerにした

GenServerの一生

./genserver.png

GenServer

defmodule MyGenServer do
  use GenServer

  def init(args) do
    # 初期化でやりたいこと
    {:ok, args}
  end


  def handle_cast(msg, state) do
    # 呼ばれたときにやりたいこと
    {:noreply, state}
  end

  def terminate(reason, state) do
    # 終端処理でやりたいこと
    reason
  end
end

Hobotのプラグイン(フレームワークからの受信)

defmodule Hobot.Plugin.Handler.Echo do
  use GenServer

  def init({context, topics} = args) do
    for topic <- topics, do: apply(context.subscribe, [topic])
    {:ok, args}
  end

  # フレームワークから欲しい情報が送られてくる
  def handle_cast({:broadcast, _topic, ref, data}, {context, _topics} = state) do
    apply(context.reply, [ref, data])
    {:noreply, state}
  end

  def terminate(reason, {context, topics}) do
    for topic <- topics, do: apply(context.unsubscribe, [topic])
    reason
  end
end

Hobotプラグインからフレームワークへ情報を伝える

フレームワークからプラグインへの情報伝達

フレームワークからプラグインへの情報伝達

./framework_to_plugin.png

フレームワークからプラグインに情報を伝えるのはRegistryを使った

PubSubの例

PubSubの絵

./registry.png

PubSub

defmodule MyMod do
  def subscribe(name, interestings) do
    for interesting <- interestings, do: Registry.register(MyRegistry, interesting, [])
    do_loop(name)
  end
  defp do_loop(name) do
    receive do
      x -> IO.inspect({name, x})
    end
    do_loop(name)
  end
end
{:ok, _} = Registry.start_link(:duplicate, MyRegistry)

spawn(fn -> MyMod.subscribe("Elixir使い", ["Ruby", "Elixir"]) end)
spawn(fn -> MyMod.subscribe("Erlang使い", ["Elixir", "Erlang"]) end)

Registry.dispatch(MyRegistry, "Elixir", fn entries ->
  for {pid, _} <- entries, do: send(pid, "Elixirにまつわるニュース")
end)
# {"Elixir使い", "Elixirにまつわるニュース"}
# {"Erlang使い", "Elixirにまつわるニュース"}

Registry.dispatch(MyRegistry, "Ruby", fn entries ->
  for {pid, _} <- entries, do: send(pid, "Rubyにまつわるニュース")
end)
# {"Elixir使い", "Rubyにまつわるニュース"}

Registryの制約

Registry.register(registry, pid, key, value) # x
Registry.register(registry, key, value)      # o

(再掲) Hobotプラグイン

defmodule Hobot.Plugin.Handler.Echo do
  use GenServer

  def init({context, topics} = args) do
    for topic <- topics, do: apply(context.subscribe, [topic])
    {:ok, args}
  end

  # (略)
end

フレームワークからプラグインへの情報の伝達

def dispatch(application_process, topic, message) do
  Registry.dispatch(application_process.pub_sub, topic, fn entries ->
    for {pid, before_receive} <- entries do
      cast_to_process(application_process, pid, message, before_receive)
      # cast_to_process の中で GenServer.cast(pid, value) している
    end
  end)
end

プラグインで起きたエラーのフレームワークでのハンドリング

プラグインで起きたエラーのフレームワークでのハンドリング

./when_error_occured.png

プラグインでエラーが起きたとき,フレームワークはどう対処するか

Supervisor

Supervisorがやること

./supervisor.png

Supervisor例

defmodule Div do
  use GenServer
  def start_link(x) , do: GenServer.start_link(__MODULE__, x, name: __MODULE__)
  def init(x), do: {:ok, x}
  def handle_call(request, _from, x), do: {:reply, x / request, x}
end
{:ok, sup} = Supervisor.start_link([{Div, 12}], strategy: :one_for_one)
GenServer.call(Div, 6) # => 2.0
GenServer.call(Div, 0) # => error
GenServer.call(Div, 3) # => 4.0

Hobotのプラグインのエラーハンドリング

まとめ

はなせなかったことたち

Hobotという名前の由来

趣味ツールを標準に寄せる窮屈さはなかったの

使ってる?

Elixirならマクロを使えば記述量を減らせるのでは

なぜcontextを関数呼び出しapply(context.register, [topic])にしたの

今後の予定