Elixir 1.4 で入った Registry のドキュメントを読んでいると唐突に出てくる via_tuple についてです。Registry とGenServer のドキュメントをちゃんと読んでいる人にとってはどうでもいい記事です。
Registry には大きく分けて2つの用途があります。
- 動的に立ち上げるプロセスに atom 以外の一意な名前をつける
- 複数のプロセスに同じ名前をつけて一括でメッセージを送る
Registryの起動時に 1は:unique
, 2 は:duplicate
を指定します。
:unique
な Registry にプロセス(GenServer)を登録する場合、:name
に via_tuple を指定するのが一般的です。
via_tuple
def start_link(name) do
GenServer.start_link(__MODULE__, name, [name: via_tuple(name)])
end
defp via_tuple(name) do
{:via, Registry, {PlayerRegistry, name}}
end
def push(name, message) do
# 呼び出し側は pid を知らなくてもメッセージを送ることができる
GenServer.cast(via_tuple(name), message)
end
なぜこれが動くかというと GenServer.castの第一引数の型 server
は以下のようになっていて atom の他に {:via, module, term}
が使えるからです。
defmodule GenServer do
@type server :: pid | name | {atom, node}
@type name :: atom | {:global, term} | {:via, module, term}
...
@spec call(server, term, timeout) :: term
def call(server, request, timeout \\ 5000) do
...
end
end
via_tupleに渡すmodule
via_tuple の2要素め, module
に Registry を指定しましたが、実は別のモジュールも渡せます。
GenServerのドキュメント をみると
* `{:via, module, term}` - the GenServer is registered with the given
mechanism and name. The `:via` option expects a module that exports
`register_name/2`, `unregister_name/1`, `whereis_name/1` and `send/2`.
One such example is the [`:global` module](http://www.erlang.org/doc/man/global.html) which uses these functions
for keeping the list of names of processes and their associated PIDs
that are available globally for a network of Elixir nodes. Elixir also
ships with a local, decentralized and scalable registry called `Registry`
for locally storing names that are generated dynamically.
:via に渡すモジュールは以下の4つの関数を実装しているモジュールであればなんでもいいようです。
register_name/2
unregister_name/1
whereis_name/1
send/2
ということは Erlang のglobal モジュールや gproc も使えますし 意味があるかはさておき Process Registry を自作することもできます。
gen.erl
gen_server.exの該当箇所を読むと、この{:via, module, name} でプロセスをひく機能自体は Erlang の gen.erl で提供されているものであることがわかります。gen_server.erl ではなく gen.erl を使っているのは link するかしないかを引数で指定したいからですね。
defp do_start(link, module, args, options) do
case Keyword.pop(options, :name) do
{nil, opts} ->
:gen.start(:gen_server, link, module, args, opts)
{atom, opts} when is_atom(atom) ->
:gen.start(:gen_server, link, {:local, atom}, module, args, opts)
{{:global, _term} = tuple, opts} ->
:gen.start(:gen_server, link, tuple, module, args, opts)
{{:via, via_module, _term} = tuple, opts} when is_atom(via_module) ->
:gen.start(:gen_server, link, tuple, module, args, opts)
{other, _} ->
raise ArgumentError, """
expected :name option to be one of:
* nil
* atom
* {:global, term}
* {:via, module, term}
Got: #{inspect(other)}
"""
end
end