Dockerを使ってElixir 1.12で増えた関数を調べたい

@torifukukaiou さんが以前取り組まれた内容を複数の[Docker]コンテナを使ってやってみるという企画です。
あえて違ったアプローチで取り組みます。
執筆時点で最新のElixirバージョンは1.14.2なのですが、あえてオリジナルの記事と同じ1.12.1を対象とします。

https://qiita.com/torifukukaiou/items/f4bade3261e14de18797

https://qiita.com/torifukukaiou/items/7230861686ad5adf31f5

https://qiita.com/mnishiguchi/items/d75668aa5c458644d759

## やりたいこと

- Elixir 1.12で追加された関数、削除された関数をコード上で確認する
- Elixirバージョンごとにノードをたてて、[:application.get_key/2]の結果を比較する
- ノードは別々の[Docker]コンテナから起動する

## ノードとは

[分散Erlangのドキュメント][Distributed Erlang]によると

> A distributed Erlang system consists of a number of Erlang runtime systems communicating with each other. Each such runtime system is called a node.

- [分散Erlangシステム][Distributed Erlang]は複数の[Erlangランタイムシステム][Erlang Runtime System]がお互いに通信することで構成される。
- [分散Erlangシステム][Distributed Erlang]上の各ランタイムシステムはノードと呼ばれる。

論よりRun
:speaking_head: < :computer:

## Elixirバージョンごとに[Docker]コンテナを準備

まずは[hexpm/elixir]の[Docker]イメージを使用して[Elixir]が使える仮想[Linux]マシンを立ち上げます。

### Elixir 1.11.4用の仮想[Linux]マシン

```sh
docker run --rm -it hexpm/elixir:1.11.4-erlang-24.3.4.6-alpine-3.15.6 /bin/sh
```

https://hub.docker.com/layers/hexpm/elixir/1.11.4-erlang-24.3.4.6-alpine-3.15.6/images/sha256-b6f5a73d866e201985e067223ff40473b0bb06d64824cf5a1d50b275d96dcdb8

### Elixir 1.12.1用の仮想[Linux]マシン

```sh
docker run --rm -it hexpm/elixir:1.12.1-erlang-24.3.4.6-alpine-3.15.6 /bin/sh
```

https://hub.docker.com/layers/hexpm/elixir/1.12.1-erlang-24.3.4.6-alpine-3.15.6/images/sha256-d53897316df2de215a237292f53aad9500945611e5e5213a634b368ff9734185

### 仮想マシンのIPアドレスを探す

他の仮想マシンと接続するにはIPアドレスが必要になりますので、それぞれの仮想マシンのIPアドレスを調べます。いろんなコマンドがありますが、ここでは`ifconfig`を使用します。

```sh
ifconfig eth0 | grep inet
```

### ノードを起動

ノードの名前と[Erlangクッキー][Node.set_cookie/2]を指定して[IEx]を起動することにより、ノードが立ち上がります。

```sh
iex --name hoge@172.17.0.7 --cookie mycookie
```

#### ノードの名前

`--name`オプションにノードの場所（IPアドレス）を含む完全修飾名を指定します。
IPアドレスはご自身のものに置き換えてください。
`@`の左側は任意の文字列で大丈夫です。

#### [Erlangクッキー][Node.set_cookie/2]

ノードが接続する際にクッキーが必要になりますので、`--cookie`オプションにクラスター共通の文字列を渡します。あるクラスターに属するすべてのノードが同じクッキーを共有することになります。

以上の作業を別々の[シェル][コマンドラインインタプリタ]から実行し、別々の仮想マシンを立ち上げ、ノードを起動します。

## [分散Erlangシステム][Distributed Erlang]を構築する

### ノードを接続

`hoge`ノードの[IEx]から`fuga`ノードに接続します。

```elixir:hogeノードのIEx
:pong = Node.ping(:"fuga@172.17.0.8")
```

[Node.ping/1]は、成功した場合は `:pong`、失敗した場合は`:pang`を返します。IPアドレスはご自身のものに置き換えてください。

[Node.list/0]で現在接続されているノードのリストが確認できます。

![ノードを接続.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82804/74930a6f-c05d-6fa3-5932-f5fa495adf9a.png)

## 別ノード内でコードを実行できるか確認

`hoge`ノードの[IEx]から`fuga`ノードに入ってコードを実行します。 試しに[IEx.Helpers.runtime_info/0]でシステムの情報を取得してみます。

```elixir:hogeノードのIEx
# hogeノード自身のシステム情報
IO.puts(System.version)
runtime_info

# fugaノードのシステム情報
Node.spawn_link(:"fuga@172.17.0.8", fn -> IO.puts(System.version) end)
Node.spawn_link(:"fuga@172.17.0.8", &runtime_info/0)
```

![別のノードに入ってコードを実行](https://user-images.githubusercontent.com/7563926/204162691-e809fb75-098d-4888-abbd-dbb371c29652.png)

https://qiita.com/mnishiguchi/items/6443ec3b3757337df292

ちゃんとノード間で通信できることが確認できました。

## Elixirすべての関数のリストを取得するコードを定義

[:application.get_key/2]を利用して、`:elixir`アプリケーションのモジュールのリストを取得します。

```elixir:IEx
IEx.configure(inspect: [limit: :infinity])

:application.get_key(:elixir, :modules)
```

https://github.com/elixir-lang/elixir/blob/7850bdd7e882815eefd1ba4b16871b32521b8bf8/lib/iex/lib/iex/autocomplete.ex#L507

https://hexdocs.pm/iex/IEx.html#configure/1-inspect


さらにのちに関数リストの差分の計算をしやすいようにあらかじめ[:application.get_key/2]の結果を加工します。 `AllLoadedElixir.get/0`という形にまとめることにしました。

それぞれのノードのIExに以下のモジュールを貼り付けます。

```elixir:それぞれのノードのIEx
defmodule AllLoadedElixir do
  def get do
    {:ok, modules} = :application.get_key(:elixir, :modules)

    functions =
      modules
      |> Enum.filter(&String.starts_with?("#{&1}", "Elixir."))
      |> Enum.reduce([], fn mod, acc ->
        mod.__info__(:functions)
        |> Enum.map(fn {f, a} -> "#{mod}.#{f}/#{a}"end)
        |> Kernel.++(acc)
      end)
      |> Enum.map(fn "Elixir." <> mfa -> mfa end)
      |> Enum.sort()

    %{
      version: System.version(),
      otp_release: System.otp_release(),
      functions: functions
    }
  end
end
```

使い方は簡単です。

```elixir:AllLoadedElixir使い方
AllLoadedElixir.get
```

[:code.all_loaded/0]だと`:elixir`アプリケーション以外のモジュールも含んでいる印象を持ったので、[:application.get_key/2]を使って明示的に`:elixir`アプリケーションのモジュールを取得することにしました。

## 関数リストを比較

[:rpc.call/4]を用いて別ノード内でコードを実行します。[Node.spawn/2]と似ていますが、[:rpc.call/4]を使うと別ノード内で計算した結果を簡単に受信できます。

```elixir:hogeノードのIEx
# hogeノードの結果（Elixir 1.11）
old = AllLoadedElixir.get

# fugaノードの結果（Elixir 1.12）
new = :rpc.call(:"fuga@172.17.0.8", AllLoadedElixir, :get, [])

removed = old.functions -- new.functions
added = new.functions -- old.functions

length(removed)
length(added)
```

## 結果発表

### 追加された関数

```elixir
for(f <- added, do: IO.puts(['* ', f]))
```

* Calendar.ISO.parse_date/2
* Calendar.ISO.parse_naive_datetime/2
* Calendar.ISO.parse_time/2
* Calendar.ISO.parse_utc_datetime/2
* Code.cursor_context/1
* Code.cursor_context/2
* Code.ensure_compiled!/1
* Code.ensure_loaded!/1
* Date.range/3
* Date.to_iso_days/1
* DateTime.to_iso8601/3
* Enum.count_until/2
* Enum.count_until/3
* Enum.product/1
* Enum.zip_reduce/3
* Enum.zip_reduce/4
* Enum.zip_with/2
* Enum.zip_with/3
* ErlangError.error_info/2
* Float.pow/2
* IO.binstream/0
* IO.binstream/1
* IO.stream/0
* IO.stream/1
* Integer.extended_gcd/2
* Integer.pow/2
* List.first/2
* List.last/2
* Module.Types.Expr.of_expr/4
* Module.Types.Unify.collect_var_indexes/2
* Module.Types.Unify.collect_var_indexes/3
* Module.Types.Unify.lift_types/2
* Module.Types.Unify.refine_var!/4
* Module.Types.Unify.restore_var!/3
* Module.delete_definition/2
* Module.get_definition/2
* Module.reserved_attributes/0
* Range.new/3
* Range.size/1
* Registry.Supervisor.start_link/6
* Registry.values/3
* Stream.zip_with/2
* Stream.zip_with/3
* System.SignalHandler.handle_call/2
* System.SignalHandler.handle_event/2
* System.SignalHandler.handle_info/2
* System.SignalHandler.init/1
* System.shell/1
* System.shell/2
* System.trap_signal/2
* System.trap_signal/3
* System.untrap_signal/2
* Tuple.product/1
* Tuple.sum/1
* URI.decode_query/3
* URI.encode_query/2
* URI.query_decoder/2

### 削除された関数

```elixir
for(f <- removed, do: IO.puts(['* ', f]))
```

* Module.Types.Expr.of_expr/3
* Module.Types.Pattern.unify_kinds/2
* Module.Types.Unify.refine_var/4
* Module.Types.lift_type/2
* Module.Types.lift_types/2
* Registry.Supervisor.start_link/5

[Elixir]公式のリリースドキュメントにざっくりと変更内容が書かれています。

https://github.com/elixir-lang/elixir/releases/tag/v1.12.0

https://github.com/elixir-lang/elixir/releases/tag/v1.12.1

答えがあっているのかどうかは知りませんが、楽しく遊べたのでこの記事はここでお開きとさせていただきます。

## ご参考までに

https://speakerdeck.com/elijo/elixirkomiyunitei-falsebu-kifang-guo-nei-onrainbian

https://qiita.com/piacerex/items/e0b6e46b1325bb931122

https://qiita.com/torifukukaiou/items/1edb3e961acf002478fd

https://qiita.com/piacerex/items/e5590fa287d3c89eeebf


[Elixir]: https://elixir-lang.org/
[Erlang]: https://www.erlang.org/
[Phoenix]: https://www.phoenixframework.org/
[Nerves]: https://hexdocs.pm/nerves
[Livebook]: https://livebook.dev/
[IEx]: https://elixirschool.com/ja/lessons/basics/basics/#%E5%AF%BE%E8%A9%B1%E3%83%A2%E3%83%BC%E3%83%89
[Node | hexdocs]: https://hexdocs.pm/elixir/Node.html
[otp_distribution | elixirschool]: https://elixirschool.com/ja/lessons/advanced/otp_distribution
[Node.ping/1]: https://hexdocs.pm/elixir/Node.html#ping/1
[Node.connect/1]: https://hexdocs.pm/elixir/Node.html#connect/1
[Node.spawn/2]: https://hexdocs.pm/elixir/Node.html#spawn/2
[Node.list/0]: https://hexdocs.pm/elixir/Node.html#list/0
[Node.set_cookie/2]: https://hexdocs.pm/elixir/Node.html#set_cookie/2
[Node.get_cookie/0]: https://hexdocs.pm/elixir/Node.html#get_cookie/0
[Node.start/3]: https://hexdocs.pm/elixir/Node.html#start/3
[epmd]: https://www.erlang.org/doc/man/epmd.html
[Elixirの分散処理(Node)とMnesia]: https://qiita.com/sand/items/c5c9dab1dce3d5c526ee
[Elixir Distribution, ELI5]: https://medium.com/@pawel_dawczak/elixir-distribution-eli5-309193004119
[Node.start/3でエラーが出る場合にはepmdをバックグラウンド起動する]: https://qiita.com/zacky1972/items/cb3aa801a179322c56d6
[コマンドラインインタプリタ]: https://ja.wikipedia.org/wiki/%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E3%83%A9%E3%82%A4%E3%83%B3%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%97%E3%83%AA%E3%82%BF
[端末エミュレータ]: https://ja.wikipedia.org/wiki/%E7%AB%AF%E6%9C%AB%E3%82%A8%E3%83%9F%E3%83%A5%E3%83%AC%E3%83%BC%E3%82%BF
[メッシュネットワーク]: https://ja.wikipedia.org/wiki/%E3%83%A1%E3%83%83%E3%82%B7%E3%83%A5%E3%83%8D%E3%83%83%E3%83%88%E3%83%AF%E3%83%BC%E3%82%AF
[Distributed Erlang]: https://www.erlang.org/doc/reference_manual/distributed.html
[ホスト名]: https://ja.wikipedia.org/wiki/%E3%83%9B%E3%82%B9%E3%83%88%E5%90%8D
[hexpm/elixir]: https://hub.docker.com/r/hexpm/elixir
[Distributed Erlang]: https://www.erlang.org/doc/reference_manual/distributed.html
[Erlang Runtime System]: https://www.erlang.org/doc/system_architecture_intro/sys_arch_intro#erlang-runtime-system
[Linux]: https://ja.wikipedia.org/wiki/Linux
[Docker]: https://ja.wikipedia.org/wiki/Docker
[:rpc.call/4]: https://www.erlang.org/doc/man/rpc.html
[:application.get_key/2]: https://www.erlang.org/doc/man/application.html#get_key-2
[:code.all_loaded/0]: https://www.erlang.org/doc/man/code.html#all_loaded-0
[IEx.Helpers.runtime_info/0]: https://hexdocs.pm/iex/IEx.Helpers.html#runtime_info/0