useuseマクロを使うと、別のモジュールが提供する機能で自分のモジュールを手早く拡張できます。モジュールをuseすると、そのモジュールは自分のモジュールにコードを注入できます。たとえば、関数を定義したり、他のモジュールをimportやaliasしたり、モジュール属性を設定したりできます。
ExercismにあるElixirの演習のテストファイルを見たことがあれば、それらがすべてuse ExUnit.Caseで始まっていることに気づいたでしょう。このたった1行のコードが、テストモジュールでマクロtestとassertを使えるようにしています。
defmodule LasagnaTest do
use ExUnit.Case
test "expected minutes in oven" do
assert Lasagna.expected_minutes_in_oven() === 40
end
end
__using__/1マクロモジュールをuseしたときに何が起こるかは、そのモジュールの__using__/1マクロが決めています。このマクロは、オプションをまとめたキーワードリストを1つの引数として受け取り、クォートされた式を返します。このクォートされた式のコードが、useを呼び出したときに自分のモジュールへ挿入されます。
defmodule ExUnit.Case do
defmacro __using__(opts) do
# some real-life ExUnit code omitted here
quote do
import ExUnit.Assertions
import ExUnit.Case, only: [describe: 2, test: 1, test: 2, test: 3]
end
end
end
オプションは、useを呼び出すときの2番目の引数として渡せます。たとえばuse ExUnit.Case, async: trueのように書きます。明示的に指定しない場合、デフォルトで空のリストになります。
ビヘイビアを使うと、_ビヘイビアモジュール_の中にインターフェース(関数とマクロの集合)を定義し、それを後からさまざまな_コールバックモジュール_で実装できます。共通のインターフェースのおかげで、これらのコールバックモジュールは互いに置き換えて使えます。
"behaviours"はイギリス英語の綴りであることに注意してください。
ビヘイビアを定義するには、新しいモジュールを作成し、目的のインターフェースに含まれる関数のリストを指定します。各関数は、@callbackモジュール属性を使って定義します。構文は関数の型仕様(@spec)と同じです。関数名、引数の型のリスト、そして取りうる戻り値の型をすべて指定します。
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
既存のビヘイビアを自分のモジュールに追加する(コールバックモジュールを作成する)には、@behaviourモジュール属性を使います。その値には、追加するビヘイビアモジュールの名前を指定します。
次に、そのビヘイビアモジュールが要求するすべての関数(コールバック)を定義する必要があります。たとえばElixirに組み込まれているAccessやGenServerのビヘイビアなど、他の誰かのビヘイビアを実装する場合は、そのビヘイビアのすべてのコールバックのリストがhexdocs.pmのドキュメントに載っています。
コールバックモジュールは、そのビヘイビアに含まれる関数だけを実装する必要はありません。1つのモジュールで複数のビヘイビアを実装することもできます。
どの関数がどのビヘイビアに由来するかを示すには、各関数の前に@implモジュール属性を使います。その値には、このコールバックを定義しているビヘイビアモジュールの名前を指定します。
defmodule BookCollection do
@behaviour Countable
defstruct [:list, :owner]
@impl Countable
def count(collection) do
Enum.count(collection.list)
end
def mark_as_read(collection, book) do
# other function unrelated to the Countable behaviour
end
end
ビヘイビアを定義するときは、コールバックのデフォルト実装を用意できます。この実装は、__using__/1マクロのクォートされた式の中で定義します。ビヘイビアモジュールの利用者がデフォルト実装を上書きできるようにするには、関数の実装の後にdefoverridable/1マクロを呼び出します。このマクロは、関数名をキー、関数のアリティを値とするキーワードリストを受け取ります。
defmodule Countable do
@callback count(collection :: any) :: pos_integer
defmacro __using__(_) do
quote do
@behaviour Countable
def count(collection), do: Enum.count(collection)
defoverridable count: 1
end
end
end
__using__/1の中で関数を定義することは、コールバックのデフォルト実装を定義する以外の目的では推奨されません。ただし、別のモジュールで関数を定義し、それを__using__/1マクロの中でインポートすることはいつでもできます。
画家を目指している友人が、あるプロジェクトのアイデアを持ちかけてきました。彼の視覚的な創造性と、技術的な専門知識を組み合わせてみましょう。そろそろジェネラティブアートに手を出してみる時です!
制約は創造性を後押しし、プロジェクトの締め切りを短くします。そこで2人は、傑作に使う図形を円たった1つに絞ることにしました。でも、円はたくさん登場します。しかも、動き回ります! 名前は……そう、「dancing dots」に決まりです。
友人はきっと、ドットのための新しい複雑な動きを次々と考えたくなるはずです。そこでまずは、あとから新しいアニメーションを簡単に定義できるような設計を作ることからコーディングを始めましょう。
各アニメーションモジュールは、init/1とhandle_frame/3という2つのコールバックを実装する必要があります。これらをAnimationモジュールに定義します。
init/1コールバックを定義します。これはopts型の引数を1つ取り、{:ok, opts}タプルか{:error, error}タプルのいずれかを返す必要があります。このコールバックの実装では、与えられたオプションがその種類のアニメーションにとって有効かどうかを確認します。
handle_frame/3コールバックを定義します。これは3つの引数、つまりドット、フレーム番号、オプションを取ります。常にドットを返す必要があります。このコールバックの実装では、現在のフレーム番号とアニメーションのオプションに基づいて、ドットの属性を変更します。
init/1コールバックのデフォルト実装を用意するAnimationの振る舞いは、use DancingDots.Animationを呼び出すだけで他のモジュールに簡単に組み込めるようにするべきです。
これを実現するために、Animationモジュールに__using__マクロを実装し、Animationモジュールを他のモジュールの振る舞いとして設定するようにします。また、init/1コールバックのデフォルト実装も提供する必要があります。init/1のデフォルト実装は、与えられたオプションをそのまま返す必要があります。
defmodule MyCustomAnimation do
use DancingDots.Animation
end
MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}
Flickerアニメーションを実装するAnimationの振る舞いを使って、点滅するアニメーションを実装します。
このアニメーションはオプションを取らないため、デフォルトのinit/1コールバックを使う必要があります。
1つのフレームを処理するhandle_frame/3コールバックを実装します。フレーム番号が4の倍数のとき、関数はドットを元の不透明度の半分にして返す必要があります。それ以外のフレームでは、ドットをそのまま返す必要があります。
フレームは1から数えます。handle_frame/3に渡されるドットは、常に元の状態のドットであり、前のフレームの状態ではありません。
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
DancingDots.Flicker.handle_frame(dot, 1, [])
# => %DancingDots.Dot{opacity: 1, radius: 24, x: 100, y: 100}
DancingDots.Flicker.handle_frame(dot, 4, [])
# => %DancingDots.Dot{opacity: 0.5, radius: 24, x: 100, y: 100}
Zoomアニメーションを実装するAnimationの振る舞いを使って、ズームするアニメーションを実装します。
このアニメーションは1つのオプション、つまり速度を取ります。速度には任意の数値を指定できます。負の値の場合は、ドットはズームインではなくズームアウトします。
init/1コールバックを実装します。渡されたオプションが:velocityキーを持つキーワードリストであることを検証する必要があります。速度の値は数値でなければなりません。数値でない場合は、エラー"The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}"を返します。
handle_frame/3コールバックを実装します。現在のフレーム番号から1を引いた値に速度を掛けた分だけ、半径を増やしたドットを返す必要があります。
フレームは1から数えます。handle_frame/3に渡されるドットは、常に元の状態のドットであり、前のフレームの状態ではありません。
DancingDots.Zoom.init(velocity: nil)
# => {:error, "The :velocity option is required, and its value must be a number. Got: nil"}
dot = %DancingDots.Dot{x: 100, y: 100, radius: 24, opacity: 1}
DancingDots.Zoom.handle_frame(dot, 1, velocity: 10)
# => %DancingDots.Dot{radius: 24, opacity: 1, x: 100, y: 100}
DancingDots.Zoom.handle_frame(dot, 2, velocity: 10)
# => %DancingDots.Dot{radius: 34, opacity: 1, x: 100, y: 100}