2ヶ月ぶりのご無沙汰です
前回の fukuoka.ex#1 の後くらいから、福岡と東京を行ったり来たりしつつ、以下登壇/勉強会をこなし、最新技術をプロダクション採用している種々企業のスーパーエンジニアの方々にお話を聞きに行く、というハードな2ヶ月で、このElixirコラムもすっかり止まってしまいました…
- ChibiDeveloperさんのLT会
- データサイエンスLT
- CyberAgentさんのデータ分析勉強会
- RustのLT会
- ファンクション倶楽部
- IoTLT
- Fukuoka CTO meetup
- Scala福岡2017
- 「福岡x人工知能…」第1回イベント
その一方で、こうした活動の中で出会った方々と、Elixirの良さを共有する機会もあり、次に覚えるプログラミング言語としてElixirを多くの方に選んでいただけた、良質な布教活動(笑)の2ヶ月でもありました
少しでもElixirがプロダクション採用されるよう、より本番運用やデータ分析みたいな領域にアプローチする内容にてリスタートしたいと思いますので、どうぞよろしく
さて今回の連載は、Elixirで遠隔PCに侵入ハッカー気分 の続きをやると見せかけ、途中でOTPを使ったリファクタリングをしたり、「耐障害性」に分岐するようなパラレルワールド的展開をしてみようかな、と
前準備
スライドP6にある通り、mixで「Pass」というElixirプロジェクトを作成済みのところからスタートします
iexを起動しておいてください
# iex -S mix
iex>
また、スライドでは、PC間での通信をする例としていますが、説明を分かりやすくするため、サーバーもクライアントも1台のPC内で行うこととします
サーバとクライアントをGenServerで1つにまとめる
P25で定義している「pwdサーバ」は、以下の通り、File.cwd()でカレントフォルダを取得して返すだけのサーバーです
defmodule Pass do
…
def start_pwd_server() do
pid = spawn( Pass, :pwd_server, [] )
:global.register_name( :pwd, pid )
end
def pwd_server() do
receive do
{ sender_pid, _ } ->
{ :ok, result } = File.cwd()
send( sender_pid, { true, result } )
end
pwd_server()
end
…
P28で定義している「pwdクライアント」は、以下の通り、pwdサーバプロセスにメッセージを投げて、フォルダパスを待ち受けるだけのクライアントです
defmodule Pass do
…
def pwd() do
send( :global.whereis_name( :pwd ), { self(), "" } )
listen()
end
…
まず、pwdサーバを起動します
iex> Pass.start_pwd_server
このサーバに問合せをすると、カレントフォルダが取得できます
iex> Pass.pwd
/code/pass
こういったサーバ/クライアントのアプリは、機能の違いはあれど、構成やインタフェースはほぼ同じため、Elixirでは、「GenServer」という名前で汎用モジュール化されています
以下のように「use GenServer」をモジュール内に記述し、handle_call()でサーバ側処理を書けば終わりです
defmodule PassGenServer do
use GenServer
def handle_call( :pwd, _from, state ) do
{ :ok, result } = File.cwd()
{ :reply, result, state }
end
end
GenServerでのpwdを試してみましょう
GenServer.start_link()でサーバ起動し、GenServer.call()でhandle_call()に定義した処理が呼び出せます
iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
{:ok, #PID<0.297.0>}
iex> GenServer.call( pid, :pwd )
"/code/pass"
うまくいきました
GenServerで書く効果
まず、GenServer版と、そうで無い版のコードを見比べて欲しいのですが、コード量が1/3程度にスリム化されています
次に、GenServer版のコードには、本質的な処理(ここではpwd実行)しか書いておらず、プロセス起動やメッセージパッシングのような処理は一切書かなくて良くなっています
サーバとクライアントを別々に定義する必要が無くなり、1つにまとめられるので、メンテナンス性も向上しています
そして、ここからが凄いのですが、関数名と引数/返却を少し変えるだけで、非同期化対応できてしまうのです!
GenServerだと非同期化もラクラク
ここまで作ったものは、同期処理のため、スリープを入れると返ってこなくなることを、まず確認しておきます
defmodule PassGenServer do
use GenServer
def handle_call( :pwd, _from, state ) do
{ :ok, result } = File.cwd()
Process.sleep( 3000 )
{ :reply, result, state }
end
end
実行すると、処理呼び出し後、スリープ分、待たされます
iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
{:ok, #PID<0.297.0>}
iex> GenServer.call( pid, :pwd )
…
(3秒、待たされる)
…
"/code/pass"
非同期化するには、handle_call() を handle_cast() に変更して、引数_fromを削除し、返却を:noreplyに変える… たったこれだけ
:lib/pass_genserver.ex
defmodule PassGenServer do
use GenServer
def handle_cast( :pwd, state ) do
{ :ok, result } = File.cwd()
Process.sleep( 3000 )
IO.puts "on handle_cast(): " <> result
{ :noreply, state }
end
end
呼出も、GenServer.cast() に変え、試してみると...
iex> recompile()
iex> { :ok, pid } = GenServer.start_link( PassGenServer, "" )
{:ok, #PID<0.406.0>}
iex> GenServer.cast( pid, :pwd )
:ok 【←呼出後、即座に返ってくる】
iex> on handle_cast(): /code/pass 【←3秒後に表示される】
バッチリ非同期になりました
元々のコードもそんなに複雑では無かったのですが、これはそもそもElixirが、サーバ/クライアントのような、マルチプロセスを書きやすい言語だからな訳ですが、GenServerを使うと、もはや本質的な処理以外は、書かなくて良いレベルまで昇華されます、OTP恐るべし
(次回に続く)
p.s.
【お知らせ】「fukuoka.ex #2」、今月末8/24(金)に開催です
ElixirとPhoenix(高速Web・APIフレームワーク)のマルチプロセスと耐障害性をテーマに、セッションを行います
ちょっと難しそうに聞こえるテーマですが、分かりやすく噛み砕いてセッションしますし、懇親会でのご質問もドシドシ受け付けますので、ご安心ください。