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