Elixir: @impl属性を活用しよう
gumi TECH
Posted on October 16, 2018
本稿は「Elixir 1.5 で追加された @impl を活用しよう」をもとに加筆・補正し、文章を整えました。
@impl
は、ビヘイビアが正しく実装されていることを示すために加えられるモジュール属性です。ビヘイビアのコールバックを明示します。Elixir 1.5から備わったこの属性についてご説明します。
@impl
属性とは
@impl
を使わないコード
@impl
属性はオプションです。つぎのように、この属性を使わなくても、とくに警告もなくコンパイルできます。
defmodule MyApp do
@behaviour Plug
def init(opts) do
opts
end
def call(conn, _opts) do
Plug.Conn.send_resp(conn, 200, "hello")
end
end
このコードのinit/1
とcall/2
は、Plug
ビヘイビアのコールバック関数を実装したものです。けれども、コードだけからはそのことがわかりません。Plug
ビヘイビアの要求するコールバック関数が何なのかを、予め知っていなければならないのです。
@impl
宣言する
そこで、@impl
属性をつぎのように宣言します。すると、コードを読む人に「この関数はコールバック関数の実装だ」とわかり、とても優しい状態になります。
defmodule MyApp do
@behaviour Plug
@impl Plug # 追加
def init(opts) do
opts
end
@impl Plug # 追加
def call(conn, _opts) do
Plug.Conn.send_resp(conn, 200, "hello")
end
end
これだけでも、@impl
の有用性としては十分です1。けれど、さらに副次的な効果があります。
一貫性を保つ
ひとつでも@impl
宣言すると、すべてのコールバック関数に@impl
を使わなければコンパイル時に警告が出されます。
# ひとつのコールバック関数から@implの定めを外す
# @impl Plug
def call(conn, _opts) do
Plug.Conn.send_resp(conn, 200, "hello")
end
コンパイル結果:
warning: module attribute @impl was not set for function call/2 callback (specified in Plug). This either means you forgot to add the "@impl true" annotation befor
e the definition or that you are accidentally overriding this callback
lib/my_app.ex:11
逆に、コールバックでない関数に@impl
を加えても警告は示されます。
# コールバックをcallでなくca11と記述
@impl Plug
def ca11(conn, _opts) do
Plug.Conn.send_resp(conn, 200, "hello")
end
コンパイル結果:
warning: got "@impl Plug" for function ca11/2 but this behaviour does not specify such callback. The known callbacks are:
* Plug.call/2 (function)
* Plug.init/1 (function)
lib/my_app.ex:11
warning: function call/2 required by behaviour Plug is not implemented (in module MyApp)
lib/my_app.ex:1
警告はふたつあります。前者が@impl
を誤って書いたことによる警告です。後者は@behaviour Plug
を加えたのにコールバックcall/2
が定義されていないことを告げています。
このように関数名を間違えたり、引数の数が違えば、@impl
はなくても警告は出されます。ただ、前者の警告メッセージの方がよりわかりやすいといえるでしょう。
さらに、@behaviour
では防げないケースも考えられます。たとえば、Foo
ビヘイビアがfoo/0
コールバック関数を要求し、つぎのようなモジュールがあったとしましょう。
defmodule MyApp do
@behaviour Foo
@impl Foo
def foo() do
"fooooo"
end
def bar() do
"baaaar"
end
end
このとき、bar/0
はコールバックではない、ただの関数です。けれども、のちのバージョンアップによって、Foo
ビヘイビアにbar/0
コールバック関数を追加するかもしれません。すると、bar/0
に@impl
を書いていないという警告が出てくれます。もし、@impl
を使っていなかったら何の警告も出ません。たまたま既存の関数と名前が一致してコンパイルはとおっても、おそらく意図した動作にはならないでしょう。
このように、今後コールバックと名前がかぶってしまう関数も安心して書けます。@impl
により一貫性が保たれるということです。
自動で@doc false
になる
コールバックは自由に呼んでいい関数ではありません。そのため、ドキュメントを生成した際に、コールバック関数の実装は自動的にドキュメントに載らないように設定されます2。
@impl true
は使わない
ビヘイビアを指定せずに@impl true
と定めると、どのビヘイビアの実装なのかは自動的に判別されます。しかし、この機能は使わない方がよいでしょう。@behaviour
を書いてあるから、@impl true
でもどのビヘイビアかすぐに分かるのではないか、と思うかもしれません。
しかし、たとえばつぎのコードの場合、bun/0
関数がどのビヘイビアの実装なのか分かりません。これを知るには Foo.Bar.__using__/1
あたりから見ていって、どのビヘイビアを実装しているか調べる必要があります。
defmodule MyApp do
use Foo.Bar
@impl true
def bun() do
"cho"
end
end
@impl
を使うのは読みやすくすることが目的です。コールバック関数であることはわかっても、どのビヘイビアの実装なのかわからなければ意味がありません。use
をひとつでも使っているなら@impl true
を使ってはならないと決めることはできます。でも、そういう例外をつくるより、つねにビヘイビアの名前を書くというルールにしておいてもよいでしょう。
パターンマッチングする場合はすべてのコールバック関数に@impl
をつける
パターンマッチングを用いると、同じ関数の定めが何度も出てきます。その関数がコールバックならすべての定義に@impl
をつけるべきです。
# コールバック関数への宣言
@impl GenServer
def handle_call(:get_value, _from, state) do
# ...
end
# すべての同名関数に宣言する
@impl GenServer
def handle_call({:set_value, value}, _from, state) do
# ...
end
@impl
の機能としては、いずれかひとつに加えていれば効果は同じです。けれど、読みやすくする目的のためにはすべてに定めた方がよいでしょう。コードを読む人に、なぜあったりなかったりするのか、といった要らぬ疑問を与えずに済みます。
まとめ
-
@impl
はコードを読む人にとってわかりやすくなるので積極的に使いましょう。- 副次的な効果もあります。
-
@impl true
は使わないようにしましょう。 - パターンマッチングさせるときはすべての関数に
@impl
を書きましょう。
Posted on October 16, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.