LoginSignup
32
27

More than 5 years have passed since last update.

Elixir: Behaviorとはどのような機能なのか?

Last updated at Posted at 2016-04-11

簡単なロガーを作ってみる

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

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

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

defmodule Hoge do
  def foo(message) do
    MyLogger.debug message
  end
end
iex(3)> Hoge.foo "this is a debug message"
Hoge.foo "this is a debug message"
"[debug] this is a debug message"
:ok

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

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はオブジェクト指向言語ではありませんが, 同様の戦術を使うことができます.

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

defmodule MyLogger do

  @logger Application.get_env(:phoenix_sample, MyLogger)

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

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

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

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

config :phoenix_sample, MyLogger, MyLogger.Console

実際に動作を確認してみると

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は以下のように定義します

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}というパターンを返すことを表現しています.
この記法については, こちらを御覧ください.

訂正

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

Behaviourを実装するモジュールを作成する

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

  @behaviour Inspectable  

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

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

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

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

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関数を追加してまたコンパイルしてみると...

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
iex(2)> c("/home/vagrant/phoenix_sample/lib/console_logger.ex")                                                                                                                                  
[MyLogger.Console]
iex(3)> 

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

Behaviourを使うと何かいいことがあるの?

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

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

不明点

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

  @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

32
27
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
32
27