Elixir: @impl属性を活用しよう

gumitech

gumi TECH

Posted on October 16, 2018

Elixir: @impl属性を活用しよう

本稿は「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/1call/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を書きましょう。

  1. わかりやすさということだけであれば、コメントでもよいと思われるかもしれません。けれど、公式に統一された形式で書けることがとても重要です。 

  2. モジュールのドキュメントに、どのビヘイビアを実装しているのか自動的に書いてくれれば、さらによさそうに思います。しかし、そういう機能はないようです。 

💖 💪 🙅 🚩
gumitech
gumi TECH

Posted on October 16, 2018

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related