Elixirの関数を別ノードに転送して実行する

[fukuoka.ex](https://fukuokaex.connpass.com/)代表のpiacereです
今回もご覧いただいて、ありがとうございます:bow:

Elixirのメッセージパッシングには、関数を指定して送信する機能があることを、こないだ知りました

これを使って、メッセージパッシング経由で、別ノードでの関数を呼び出してみる検証を行います

なお、今回の記事は、ノード間をいったり来たりしながら、コードを書いたり、モジュールを動かしたりするので、書いている通りに実行しないと、すぐに迷子になるので、読み飛ばしながら実行するクセのある方は、注意しながら試してください
（思った以上に、書いてある通りにやらない方が多いこと、最近、分かってきました…）

特に、コードに書いてある「ノードxx側」は、キチンと確認しながら、進めていきましょう

# 2つのノードを接続する

シェルを2枚、起動し、各シェルで各々Elixir PJを作成します

```:ノード1側
mix new node1
cd node1
```

```:ノード2側
mix new node2
cd node2
```

シェルを2枚、並べて表示します
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/192173eb-7b4f-9691-8842-d1e2ac0d9da0.png)

次に、各々のiexをノード名付きで起動します

```:ノード1側
iex --name elixir_node1@127.0.0.1 -S mix
```

```:ノード2側
iex --name elixir_node2@127.0.0.1 -S mix
```

立ち上げた時点では、この2つのノードは、接続していないため、以下コマンドで確認しても、ノードのリストは空です

```elixir:ノード1側
iex> Node.list
[]
```

これを、以下コマンドで接続します

```elixir:ノード1側
iex> Node.connect( :"elixir_node2@127.0.0.1" )
[]
```

すると、ノードのリストに出てくるようになります

```elixir:ノード1側
iex> Node.list
[:"elixir_node2@127.0.0.1"]
```

反対に、ノード2側からも、ノード1に接続していることが分かります

```elixir:ノード2側
iex> Node.list
[:"elixir_node1@127.0.0.1"]
```

# ノード間で通信する

それでは、ノード1を送信側、ノード2を受信側として、ノード間の通信を行ってみましょう

まずノード2のiexのプロセスで、受信用の待受をします

```elixir:ノード2側
iex> pid = self()
#PID<0.110.0>
iex> :global.register_name( :endpoint_node1_test, pid )
iex> receive do _ -> IO.puts( "received" ) end
```

最初は、自身（≒iex）のプロセスIDを取得しており、その次で、自プロセスIDを「:endpoint_node1_test」という名前で、ノード間の名前検索を可能とする登録を行っています

最後のreceiveにて、待受状態に入り、メッセージが来たら、「received」と表示します

さて、受信側の準備が出来たので、今度はノード1から、「:endpoint_node1_test」に向けて、メッセージ送信します

```elixir:ノード1側
iex> to_pid = :global.whereis_name( :endpoint_node1_test )
#PID<14973.110.0>
iex> send( to_pid, "send from Node1" )
```

すると、ノード2側で、「received」と表示されます

```elixir:ノード2側
iex> receive do _ -> IO.puts( "received" ) end
received
:ok
```

こんな感じで、Elixirでは、カンタンにメッセージパッシングが実現できます、素晴らしい

なお待受側は、1度、処理を行うと、その後は、普通に終了するため、再度メッセージ送信しても、ノード2は、もう反応しなくなります（上記の:okで終了しています）

```elixir:ノード1側
iex> send( :global.whereis_name( :endpoint_node1_test ), "send from Node1(2nd time)" )
```

ただし、メッセージが送られたことそのものは、プロセス（≒ここではiexを起動しているプロセス）で保持されているため、ノード2の待受を再度行うと、即座に待受処理が走ります

```elixir:ノード2側
iex> receive do _ -> IO.puts( "received(2nd time)" ) end
received(2nd time)
:ok
```

「sendで投げ、プロセスにキューイングされ、receiveでキューから取り出す」という機構が、キチンと機能していることが分かります

さて、受信はしているものの、送信側から送られたメッセージは無視しているため、メッセージも受け取り、受信側で表示してみましょう

```elixir:ノード2側
iex> receive do message -> IO.puts( "received(3rd time): #{ message }" ) end
```

再度、送信します

```elixir:ノード1側
iex> send( :global.whereis_name( :endpoint_node1_test ), "send from Node1(2nd time)" )
```

受け取ったメッセージが表示されるようになりました

```elixir:ノード2側
received(3rd time): send from Node1(3rd time)
:ok
```

# サーバプロセス化（受信し続ける）

上記の処理を、モジュール化しながら、待受側を1度の受信で終わらない処理に変え、iexとは別のサーバプロセスで起動できるように変えます

以下の各ノードのモジュールファイルを作成してください

```elixir:lib/node1.ex　※ノード1側
defmodule Node1 do
	def send( message ) do
		to_pid = :global.whereis_name( :endpoint_node1 )
		IO.inspect( to_pid )

		send( to_pid, message )
	end
end
```

```elixir:lib/node2.ex　※ノード2側
defmodule Node2 do
	def listen() do
		server_pid = spawn( Node2, :exeute, [] )
		IO.inspect( server_pid )

		:global.register_name( :endpoint_node1, server_pid )
	end

	def exeute() do
		receive do
			message -> IO.inspect( message )
		end
		exeute()
	end

	def stop() do
		Process.exit( :global.whereis_name( :endpoint_node1 ), :normal )
		:global.unregister_name( :endpoint_node1 )
	end
end
```

Node2.execute()の末尾で、execute()自身を呼び出す処理を入れることで、1度の待受で終わらず、繰り返し受信できるようになります（この処理は、再帰処理のように見えますが、実際は、末尾再帰最適化が行われ、再帰では無く、ループとしてコンパイルされます）

受信側のNode2モジュールは、Node2.listen()で待受開始し、Node2.stop()で待受終了できます

```elixir:　※ノード2側
iex> recompile
iex> Node2.listen
```

送信側のNode1モジュールを使って、送信します

```elixir:　※ノード1側
iex> recompile
iex> Node1.send( "send from Node1.send()" )
```

ノード2側で受信が確認できます

```elixir:　※ノード2側
"send from Node1.send()"
```

字面だと分かりにくいかも知れないので、ノード1とノード2の処理結果を画面で見ると、こんな感じになります
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/e492d0d3-c81f-ae0c-dc32-44d13ded64e9.png)

なお、こういうクライアント／サーバを作るときは、「GenServer」を使うとラクできて良いですが、内部構造がラップされて送受信が分かりにくくなるので、今回は敢えて使わず、プリミティブなsend／receiveで作っています

# Elixirの関数を別ノードに転送して実行…されない…

さて、いよいよ本題、Elixirの関数を別ノードに転送して実行してみます

まず、ノード1側に、転送する関数のモジュールを作ります

```elixir:lib/remote.ex　※ノード1側
defmodule Remote do
	def procedure() do
		IO.puts( "PID=#{ inspect( self() ) }: I'm Remote.procedure()" )
	end
end
```

「&」とアリティを指定して、関数を送信します

```elixir:　※ノード1側
iex> recompile
iex> Node1.send( &Remote.procedure/0 )
```

ノード2には、関数がインスペクトされます

```elixir:　※ノード2側
&Remote.procedure/0
```

関数が渡っていることが確認できたので、受信側で、受け取った関数を呼び出す改修を行います

```elixir:lib/node2.ex　※ノード2側
defmodule Node2 do
	…
	def exeute() do
		receive do
			message -> 
				IO.inspect( message )
				message.()
		end
		exeute()
	end
	…
```

ノード2の待受プロセスを再起動します

```elixir:　※ノード2側
iex> recompile
iex> Node2.stop
iex> Node2.listen
```

再度、関数を送信します

```elixir:　※ノード1側
iex> recompile
iex> Node1.send( &Remote.procedure/0 )
```

すると、「関数が存在しない」というエラーが出ます

```elixir:　※ノード2側
19:22:32.976 [error] Error in process #PID<0.225.0> on node :"elixir_node2@127.0.0.1" with exit value:
{:undef, [{Remote, :procedure, [], []}, {Node2, :exeute, 0, [file: 'lib/node2.ex', line: 13]}]}
```

ふむ、どうやら関数自体が転送される訳では無いようです

# 関数は指定時には評価されていない

試しに、ノード2の待受プロセスを起動し直して、ノード1で未定義の関数を呼び出してみましょう

```elixir:　※ノード2側
iex> Node2.listen
```

```elixir:　※ノード1側
iex> Node1.send( &Non.exist/0 )
```

すると、ノード2では、やはりエラーが出ますが、ノード1では、特にエラーは出ません

つまり、「&」とアリティで指定する関数は、その時点では定義済みか確認されず、エラーも出ない、ということです…動的な関数バインディングですね

送信しているのは、「関数そのもの」では無く、「呼び出す関数」ということです

# 実行ノードにモジュールを転送すれば実行できる

ノード1にモジュールを置いてても、ノード2で実行することはできないので、ノード2にモジュールを転送してみましょう

以下コマンドで、接続されているノード全てに、モジュールを配布できます

```elixir:　※ノード1側
iex> nl( Remote )
{:ok, [{:"elixir_node2@127.0.0.1", :loaded, Remote}]}
```

実際に配布されたか、ノード2で単品実行してみます

```elixir:　※ノード2側
iex> Remote.procedure
PID=#PID<0.110.0>: I'm Remote.procedure()
:ok
```

それでは、関数呼び出しを送信してみましょう

```elixir:　※ノード1側
iex> Node1.send( &Remote.procedure/0 )
```

```elixir:　※ノード2側
&Remote.procedure/0
PID=#PID<0.234.0>: I'm Remote.procedure()
```

うまくいきました

画面で見ると、こんな感じになりました
![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/b0aea15d-38a2-574b-0b57-e40aa42062e9.png)

# オマケ：リモート端末に侵入した気分を味わう

以下のような送信を行うと、まるでリモート端末に侵入した気分が味わえると思います（実際に動かした方だけが体験できるよう、結果は伏せておきます）:kissing_smiling_eyes:

```elixir:　※ノード1側
iex> recompile
iex> Node1.send( &h/0 )
```

```elixir:　※ノード1側
iex> recompile
iex> Node1.send( &pwd/0 )
```

```elixir:　※ノード1側
iex> recompile
iex> Node1.send( &ls/0 )
```

# 終わり

Elixirの関数を別ノードに転送して実行してみました

ちなみに、試した結果、関数自体が転送される訳では無いことが分かりましたので、関数は、最初から受信側ノード（今回だとノード2）で定義すればOKです

それと、メッセージパッシングで双方向の通信を行うこともできます

- 送信側は、sendした後にreceiveで待受にする
- 受信側は、receiveハンドラ内で送信元PIDにsendする

余力があれば、チャレンジしてみてください

# p.s.「いいね」よろしくお願いします

ページ左上の ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/4d515047-cc48-382e-c2b1-3ad0cc50dbbf.png) や ![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/a4e3da58-70a3-4197-95a2-6a6906650d01.png) のクリックを、どうぞよろしくお願いします:bow:
ここの数字が増えると、書き手としては「ウケている」という感覚が得られ、連載を更に進化させていくモチベーションになりますので、もっとElixirネタを見たいというあなた、私達と一緒に盛り上げてください！:tada: