Elixir: Behaviourとはどのような機能なのか？

# 簡単なロガーを作ってみる
話の枕として、受け取った文字列をちょっと加工して標準出力に出力するロガーというものを考えてみます.
コードは至って簡単で, 以下のとおり.

```ex
defmodule MyLogger do
  def debug(message) do
    IO.inspect "[debug] " <> message
    :ok
  end
end
```

テスト用のモジュールを書いて, 動作を確認してみましょう.

```ex
defmodule Hoge do
  def foo(message) do
    MyLogger.debug message
  end
end
```

```ex
iex(3)> Hoge.foo "this is a debug message"
Hoge.foo "this is a debug message"
"[debug] this is a debug message"
:ok
```

動作も問題ないようです.
ところがある日, 標準出力に出力するのは使い勝手が悪いので, やっぱりファイルに出力してほしいと依頼されました.
早速, 元のコードを以下のように修正します.

```ex
defmodule MyLogger do                                                                                                                                                                            
  def debug(message) do
    {:ok, file} = File.open "debug.log", [:append]                                                                                                                                             
    IO.puts file,  "[debug] " <> message                                                                                                                                                       
    :ok
  end
end
```

言うまでもないことですが, この方法は悪手です.
この先, ログをファイルではなくログサーバへ投げたい, Slackに投げたいといった要求が出ることは十分予想されますし, 実行される環境によって出力先を変えたいという要求がでることも十分に考えられるのに
その度にソースを書き換えて, コンパイルして, などとやるのは冗長というか場当たり的というかすぎる方法だといえます.

私が以前（10年くらい前？）主に関わっていたJavaの世界では, こういった場合ロガークラスに直接処理を書くのではなく
ログの出力先ごとにクラスを作成し, ロガークラスは設定ファイルなどで指定されたクラスに処理を委譲するのが良いとされていました.(最近の動向はわかりませんが)

Elixirはオブジェクト指向言語ではありませんが, 同様の戦術を使うことができます.

つまり, ロガーモジュールのログ出力関数では直接ログの出力をするのではなく, 委譲先のモジュールの関数を呼び出すように変更し...

```ex
defmodule MyLogger do

  @logger Application.get_env(:phoenix_sample, MyLogger)

  def debug(message) do
    @logger.debug message
  end
end
```

それとは別に特定ターゲットに対してログ出力を実行するモジュールを用意し...

```ex
defmodule MyLogger.Console do
  def debug(message) do
    IO.inspect "[debug] " <> message
  end
end
```

最後に, 設定ファイルでロガーモジュールの委譲先モジュールを指定してやれば完成.

```ex
config :phoenix_sample, MyLogger, MyLogger.Console
```
実際に動作を確認してみると

```ex
iex(5)> Application.get_env :phoenix_sample, MyLogger
Application.get_env :phoenix_sample, MyLogger
MyLogger.Console #<--- 委譲先モジュール
iex(6)> Hoge.foo "this is debug message"
Hoge.foo "this is debug message"
"[debug]this is debug message"
:ok
```

この通り, 想定通りの動きをしている様子がわかります.
あとは異なる出力先ごとにモジュールを追加していき, 使いたいモジュールを設定ファイルに書いていくだけで簡単にログの出力先を切り替えることができます.

しかし, この方法には一つ重大な問題点が潜んでいます.
それは, ___委譲先のモジュールに, 呼び出される予定の関数が実装されている保証がない___ことです.

# Behaviourの登場
## Behaviourとはなにか
上記のような状況で現れるのが, Behaviourです.
このBehaviorとは何かというと, 作ろうとしているモジュールに特定の関数群が実装されているかを, コンパイラがチェックできるようにするための機能です.
JavaのInterfaceに似たもの(あくまで似たもの)だと私は理解しています.

## Behaviourを定義する
Behaviourは以下のように定義します

```ex
defmodule Inspectable do
  use Behaviour

  @callback debug(message :: String.t) :: :ok | {:error, term}

  @callback info(message :: String.t) :: :ok | {:error, term}

end
```

まず, Behaviourモジュールをuseし, 実装させたい関数定義を~~defcallbackマクロ~~@callbackアトリビュートを使って定義していきます.
ちなみに実装させたいものが関数ではなくマクロの場合は代わりに~~defmacrocallback~~@macrocallbackを使うようです.
~~defcallbackマクロ~~@callbackには, 随分と見慣れない表現の引数が渡されていますが, これはElixirのfunction specificationに合わせた記法であり, 例えばdebug関数であれば, 
String.t型の引数を一つ受け取り, `:ok`もしくは`{:error, term}`というパターンを返すことを表現しています.
この記法については, [こちら](http://elixir-lang.org/docs/v1.1/elixir/Kernel.Typespec.html)を御覧ください.

### 訂正
最初, この説では「関数定義にはdefcallbackマクロを使う」と書きましたが, 現在このマクロは非推奨となっており, 代わりに@callbackアトリビュートを使用することが推奨されています.
本文もこれに合わせて修正しました.

## Behaviourを実装するモジュールを作成する
前の節で定義したInspectableを, 特定モジュールに実装させたい時は, そのモジュールに

```ex
  @behaviour Inspectable  
```

というアトリビュートを追加してやるだけでOKです.
実際に, 標準出力へのログ出力を行うMyLogger.Consoleモジュールに, Inspectableを実装させてみましょう.

```ex
defmodule MyLogger.Console do
  # Behaviourt追加
  @behaviour Inspectable

  def debug(message) do
    IO.inspect "[debug] " <> message
  end
end
```

このモジュールをコンパイルしてみると... 

```ex
iex(2)> c("/home/vagrant/phoenix_sample/lib/console_logger.ex")                                                                                                                                  
console_logger.ex:1: warning: undefined behaviour function info/1 (for behaviour Inspectable)
[MyLogger.Console]
```

すると, このようにInspectableBahaviourが要求している`info/1`関数が定義されていないというワーニングがでます.（なぜエラーではなくワーニングなんだろう…）

`info/1`関数を追加してまたコンパイルしてみると...

```ex
defmodule MyLogger.Console do
  @behaviour Inspectable

  def debug(message) do
    IO.inspect "[debug] " <> message
  end

  def info(message) do
    IO.inspect "[info] " <> message
  end
end
```

```ex
iex(2)> c("/home/vagrant/phoenix_sample/lib/console_logger.ex")                                                                                                                                  
[MyLogger.Console]
iex(3)> 
```

今度はワーニングが出ることもなく, 無事コンパイルが成功しました. 

## Behaviourを使うと何かいいことがあるの？
Behaviourを使うことで, 処理の流れは実装されているが, 細部の実装については利用者の都合に合わせて自由に拡張することが可能なライブラリ, つまりフレームワークが作りやすくなるんじゃないでしょうか.

実際自分で使うことになるかどうかは, 何を作ろうとしているかによってしまうとは思うのですが
ライブラリなどを見ているといたるところで使われているので, どんなものかだけでも知っておくと
ソースを追う助けにはなると思います.

## 不明点
Behaviourを使うことで, モジュールに特定の関数の実装を強制することができることはわかったのですが
そのモジュールを実際に使う場面, ロガーの例で言うと

```ex
  @logger.debug message
```

と書かれている部分で, `@logger`のモジュールがInspectableBehaviourを実装しているかどうかをコンパイル時にチェックする機構がなくて, 
結局実行してみないことにはわからないという問題はそのままに思えるのは, 私の調査不足ですかね？

# 参考
14 モジュールのアトリビュート - Module attributes : http://elixir-ja.sena-net.works/getting_started/14.html
Behaviour : http://elixir-lang.org/docs/stable/elixir/Behaviour.html
Kernel.Typespec : http://elixir-lang.org/docs/v1.1/elixir/Kernel.Typespec.html