Erlang/OTP: タプル・モジュールでインターフェイスと実装モジュールを分離する

Erlangでは、関数呼び出しの際に、`モジュール名:関数名(引数, …)` と書く代わりに、任意のタプル `{モジュール名, 任意の要素, …}` を使って、`{モジュール名, 任意の要素, …}:関数名(引数, …)` と書くことができる。このような書き方を、タプル・モジュール（tuple module）と呼ぶ。

タプル・モジュールで呼び出された関数には、最後の引数にそのタプルがバインドされる。

例：

```erlang:my_module.erl
-module(my_module).
-export([my_fun/3]).

my_fun(X, Y, Tup) ->
  {X, Y, Tup}.
```

実行結果

```erl
> {my_module, a, b}:my_fun(1, 2).
{1,2,{my_module,a,b}}.
```

この仕組みを使って、共通のインターフェイス（API）を提供するモジュールと、そのAPIを実装するモジュールを分離してみよう。

# ordmaps：汎用的なOrdered Map

ここでは、例として、要素がキーの昇順でソートされたmapを提供する汎用的なAPI `ordmaps` を定義する。そして、そのAPIを、stdlibの `gb_trees` と、Erlang/OTP 17の組み込みデータ型の `map` の２種類のデータ構造で実装する。

## ordmaps モジュールの使用例

```erlang
01: M0 = ordmaps:new(gb_tree),
02: M1 = M0:add(b, 1),
03: M2 = M1:add(a, 1),
04: M3 = M2:update(b, 2),
05: ?assertEqual({ok, 1}, M3:find(a)),
06: ?assertEqual([a, b], M3:keys()),    %% キーがソートされている。
```

01行のように、`ordmaps:new(gb_tree)` とすると、`gb_trees` による実装モジュールが使われる。また、`ordmaps:new(map)` とすれば、mapを用いた実装モジュールが使われる。

どちらの実装モジュールを使う場合でも、

1. 呼び出し側のコードに一切変更が必要ない
2. 呼び出し側では実装モジュールの内容について、何も知る必要がない

ところが、ポイントだ。

こうすれば、例えば、環境や設定によって実装モジュールを切り替える、といったことが、エレガントに実現できる。

# ordmapsのAPI

`ordmaps` のspecを見てみよう。とりあえず以下の関数を用意した。

```erlang:ordmaps.erl
-module(ordmaps).

-export(
   [new/1,
    add/3,
    update/3,
    find/2,
    remove/2,
    keys/1,
    to_list/1
   ]).

-spec new(map_type()) -> ordmap().
-spec add(key(), value(), ordmap()) -> ordmap() | no_return().
-spec update(key(), value(), ordmap()) -> ordmap() | no_return().
-spec find(key(), ordmap()) -> {ok, value()} | not_found.
-spec remove(key(), ordmap()) -> ordmap().
-spec keys(ordmap()) -> [key()].
-spec to_list(ordmap()) -> [key()].
```

型 `map_type/0`、`ordmap/0`、`key/0` などの定義はこうなる。

```erlang:ordmaps.erl
-record(?MODULE, {
           impl_mod :: module(),
           data :: term()
          }).

-type map_type() :: map | gb_tree.
-type ordmap()   :: #?MODULE{}.
-type key()      :: term().
-type value()    :: term().
-type data()     :: map() | gb_tree().
```

`?MODULE` の部分は、コンパイル時にモジュール名 `ordmaps` で置き換えられるので、こう書くのと同じだ。

```erlang:ordmaps.erl
-record(ordmaps, {
           impl_mod :: module(),
           data :: term()
          }).
-type ordmap() :: #ordmaps{}.
```

このレコードが、タプル・モジュールになる。`impl_mod` には、実装モジュールの名前を、`data` には実装モジュールが使用するデータ（gb_tree、または、mapデータ）をバインドする。

**例：gb_treesの場合**

```erlang
{ordmaps,
 ordmaps_impl_gb_trees,
 {3,{{b,name},"b",
      {{b,count},2,
        {{a,name},"a",nil,...},
        nil},
      nil}}}
```

**例：mapの場合**

```erlang
{ordmaps, 
 ordmaps_impl_maps,
 #{{a,name} => "a",{b,count} => 2,{b,name} => "b"}}
```

# ordmapsモジュールと実装モジュールの関数定義

## ordmaps:new/1

まず、`ordmaps:new/1` を定義しよう。

```erlang:ordmaps.erl
-spec new(map_type()) -> ordmap().
new(map) ->
    ImplMod = ordmaps_impl_maps,
    Data = ImplMod:new(),
    #?MODULE{impl_mod=ImplMod, data=Data};
new(gb_tree) ->
    ImplMod = ordmaps_impl_gb_trees,
    Data = ImplMod:new(),
    #?MODULE{impl_mod=ImplMod, data=Data}.
```

引数 `map_type` によって、実装モジュール `ordmaps_impl_maps` または `ordmaps_impl_gb_trees` を選択し、一方のモジュールの `new/0` を呼んで、空のデータ構造を作成する。最後にそのデータ構造と実装モジュール名をタプル・モジュールにセットして、呼び出し側に返す。

## ordmaps_impl_gb_trees:new/0

実装モジュール側ではこのように定義すればよい。

```erlang:ordmaps_impl_gb_trees.erl
-spec new() -> ordmaps:data().
new() ->
    gb_trees:empty().
```

## ordmaps_impl_maps:new/0

mapによる実装はこのとおり。`#{}` は空のmap。

```erlang:ordmaps_impl_maps.erl
-spec new() -> ordmaps:data().
new() ->
    #{}.
```

## ordmaps:add/3

次は `ordmaps:add/3` の定義。

```erlang:ordmaps.erl
-spec add(key(), value(), ordmap()) -> ordmap() | no_return().
add(Key, Val, #?MODULE{impl_mod=ImplMod, data=Data}=Ordmap) ->
    Data1 = ImplMod:add(Key, Val, Data),
    Ordmap#?MODULE{data=Data1}.
```

３番目の引数としてタプル・モジュールが渡されるので、`ImplMod` と `Data` にバインドする。`ImplMod:add/3` で実装モジュール側の関数を呼び出すと、更新されたデータ構造が返ってくるので、タプル・モジュールの `data` フィールドにセットして、呼び出し元に返す。

## ordmaps_impl_gb_trees:add/3

実装モジュール側の定義。特に難しいところはない。

```erlang:ordmaps_impl_gb_trees.erl
-spec add(ordmaps:key(), ordmaps:value(), ordmaps:data())
         -> ordmaps:data() | no_return().
add(Key, Val, Tree) ->
    gb_trees:insert(Key, Val, Tree).
```

## ordmaps_impl_maps:add/3

map側の定義。こちらは `maps:is_key/2` によるキーの存在チェックが入っているものの、単純な実装だ。

```erlang:ordmaps_impl_maps.erl
-spec add(ordmaps:key(), ordmaps:value(), ordmaps:data())
         -> ordmaps:data() | no_return().
add(Key, Val, Map) ->
    case maps:is_key(Key, Map) of
        true ->
            error({key_exists, Key});
        false ->
            maps:put(Key, Val, Map)
    end.
```

あとは、これの繰り返しだ。

# 完成したプログラム

完成したプログラムを GitHubの [tatsuya6502/erlang-examples/](https://github.com/tatsuya6502/erlang-examples/) に置いたので、ぜひ実行してみてほしい。

```shell-session
$ git clone https://github.com/tatsuya6502/erlang-examples.git
```

コードだけ読みたい場合は、このリンクから：

- [ordmaps.erl](https://github.com/tatsuya6502/erlang-examples/blob/master/tuple-module/src/ordmaps.erl)
- [ordmaps_impl_gb_trees.erl](https://github.com/tatsuya6502/erlang-examples/blob/master/tuple-module/src/ordmaps_impl_gb_trees.erl)
- [ordmaps_impl_maps.erl](https://github.com/tatsuya6502/erlang-examples/blob/master/tuple-module/src/ordmaps_impl_maps.erl)
- [test/eunit/ordmaps_tests.erl](https://github.com/tatsuya6502/erlang-examples/blob/master/tuple-module/test/eunit/ordmaps_test.erl)

今回は Erlang/OTP 17の map型を使用したので、実装モジュールの ordmaps_impl_maps モジュールをコンパイルするには、Erlang/OTP 17AのRC1以降のインストールが必要になる。その方法については、[こちら](http://qiita.com/tatsuya6502/items/1fd14a8a7527e0fcca47)に書いたので参考にしてほしい。

逆に、17より以前のバージョンでコンパイルするには、以下のようにする。

1. tuple-module/src/ordmaps_impl_maps.erlを削除する
2. tuple-module/rebar.config 中の `{require_otp_vsn, "17"}` を `{require_otp_vsn, "R15|R16|17"}` に変更する。

## ビルドとEUnitケースの実行

`make clean eunit` でテストケースが実行される。テストケースの内容については、上記の ordmaps_tests.erl へのリンクを参照。

```shell-session
$ cd erlang-examples/tuple-module/
$ make clean eunit
...

test/eunit/ordmaps_test.erl:37:<0.58.0>: M = {ordmaps,ordmaps_impl_maps,#{}}
test/eunit/ordmaps_test.erl:48:<0.58.0>: M6 = {ordmaps,ordmaps_impl_maps,#{{a,name} => "a",{b,count} => 2,{b,name} => "b"}}
test/eunit/ordmaps_test.erl:37:<0.58.0>: M = {ordmaps,ordmaps_impl_gb_trees,{0,nil}}
test/eunit/ordmaps_test.erl:48:<0.58.0>: M6 = {ordmaps,ordmaps_impl_gb_trees,
              {3,{{b,name},"b",{{b,count},2,{{a,name},"a",nil,...},nil},nil}}}
  All 2 tests passed
```


# 仕上げ１：behaviourを定義して、コンパイラーによるチェックを強化する

このままでも問題なく動くのだが、もし、実装モジュール側で、関数の実装を忘れてしまったり、関数名やarity（引数の数）を間違えてしまったらどうなるだろうか？　例えば、`add(Key, Val, Map)` とするところ、`add(Key, Val)` と定義してしまったら？　Erlangは動的型付き言語なので、このようなエラーがあっても、コンパイル時に検出できない。プログラムを実行して、初めてエラーに気付く。

テストケースをしっかり書けば問題ない、という意見もあるが、ここは、コンパイラーに検出させたいところだ。Erlangには、これを実現する仕組みの一つとして、behaviorがある。

## ordmapsのbehaviour定義

まず `ordmaps` モジュールに、callbackを定義する。

```erlang:ordmaps.erl
-callback new() -> data().
-callback add(key(), value(), data()) -> data() | no_return().
-callback update(key(), value(), data()) -> data() | no_return().
-callback find(key(), data()) -> {ok, value()} | key_not_found.
-callback remove(key(), data()) -> data().
-callback keys(data()) -> [key()].
-callback to_list(data()) -> [{key(), value()}].
```

なお、この callback を使った表記法は Erlang/OTP R15B で導入されたので、それより以前のバージョンでは使用できない。それらのバージョンでは、`behaviour_info/0`という関数を定義すればよい。

## 実装モジュール側で、behaviourを実装することを宣言

実装モジュール側では、`ordmaps` のbehaviourを実装することを宣言する。

```erlang:ordmaps_impl_gb_trees.erl
-behaviour(ordmaps).
```

こうすることで、関数の実装漏れ、関数名やarityの間違いを、コンパイラーが検出できるようになる。

**TODO**: エラー検出の例


# 仕上げ２：specを書いて、Dialyzerでチェックする

残念ながら、コンパイラーは関数のarity（引数の数）までしかチェックできないので、behaviourによる方法だけでは、型の間違いまでは検出できない。例えば `add(Key, Val, Map)` とするところを、`add(Map, Key, Val)` と定義してしまっても、コンパイルは通ってしまう。

このようなエラーは、Dialyzerで検出できる。DialyzerはErlang/OTPに付属する静的コード解析ツールで、関数の引数や戻り値の型を中心に、様々なチェックを行うことができる。ただし、Dialyzerで精度の高い解析を行うには、type定義や、関数のspecをできるだけ具体的に書く必要がある。GitHub上の完成したプログラムでは、このあたりがしっかり定義されているので、参考にしてもらいたい。個人的には、コードを書く前に、specを書くのがお気に入りのやりかただ。

Dialyzerによるチェックは `make dialyze` で実行できる。もし、HOMEディレクトリに `.dialyzer_plt.17` ファイルがない場合は、その構築にしばらく時間がかかるので、コーヒーでも飲みながら待ってほしい。

```shell-session
$ cd erlang-examples/tuple-module
$ make dialyze    
...

dialyzer --plt /home/tatsuya/.dialyzer_plt.17 -Wunmatched_returns -r ./ebin
  Checking whether the PLT /home/tatsuya/.dialyzer_plt.17 is up-to-date... yes
  Proceeding with analysis... done in 0m0.39s
done (passed successfully)
```

**TODO**: エラー検出の例

# まとめ

タプル・モジュールを使うことで、インターフェイスから実装を分離する方法を紹介した。ある程度の規模のシステムになると、このような手法を使う場面が多くあるはずだ。

また、behaviourやDialyzerを使って、ビルド時にモジュール間の実装エラーを検出する方法も解説した。私のいるチームでは、Dialyzerによるチェック（と、ここでは紹介しなかった Erlang QuickCheckのステートマシーンによるテスト）は必須アイテムとなっている。とても便利なツールなので、ぜひ活用してもらいたい。