use巨集讓我們能快速用其他模組提供的功能來擴充自己的模組。當我們use一個模組時,那個模組可以把程式碼注入我們的模組中,例如定義函式、import或alias其他模組,或是設定模組屬性。
如果你看過這裡 Exercism 上某些 Elixir 練習的測試檔案,大概會注意到它們全都以use ExUnit.Case開頭。就是這一行程式碼讓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巨集。它接受一個引數,也就是帶有選項的關鍵字清單,並回傳一個引述的運算式。當我們呼叫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時可以把選項當作第二個引數傳入,例如use ExUnit.Case, async: true。如果沒有明確給定,選項會預設為空清單。
行為讓我們可以在一個_行為模組_中定義介面(一組函式和巨集),之後再由不同的_回呼模組_實作。有了共用的介面,這些回呼模組就能互換使用。
注意「behaviours」的英式拼法。
要定義一個行為,我們需要建立一個新模組,並指定屬於這個介面的函式清單。每個函式都必須用@callback模組屬性來定義。語法和函式型別規格(@spec)完全相同。我們需要指定函式名稱、引數型別的清單,以及所有可能的回傳型別。
defmodule Countable do
@callback count(collection :: any) :: pos_integer
end
要把現有的行為加入我們的模組(建立回呼模組),我們使用@behaviour模組屬性。它的值應該是我們要加入的行為模組名稱。
接著,我們需要定義那個行為模組要求的所有函式(回呼)。如果我們實作的是別人的行為,例如 Elixir 內建的Access或GenServer行為,可以在 hexdocs.pm 的文件中找到該行為所有回呼的清單。
回呼模組不限於只實作屬於它行為的函式。單一模組也可以實作多個行為。
為了標示哪個函式來自哪個行為,我們應該在每個函式前使用模組屬性@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巨集中匯入它們。
你的朋友一直想成為藝術家,他帶著一個專案構想來找你。讓我們把他的視覺創意和你的技術專長結合起來吧。該來玩玩生成式藝術了!
限制有助於激發創意,也能縮短專案的截止期限,所以你們兩個講好,這件曠世巨作只能有一種形狀:圓形。不過,圓形會有很多個,而且它們還能到處移動!你們就叫它……跳舞的點點吧。
你朋友之後一定會想為這些點設計更多花俏的新動作,所以你打算先建立一套架構,讓你之後能輕鬆定義新的動畫。
每個動畫模組都需要實作兩個回呼:init/1和handle_frame/3。請在Animation模組中定義它們。
定義init/1回呼。它應該接受一個型別為opts的引數,並回傳{:ok, opts}元組或{:error, error}元組。這個回呼的實作會檢查傳入的選項對這種特定的動畫型別是否有效。
定義handle_frame/3回呼。它應該接受三個引數:點、影格編號和選項。它應該一律回傳一個點。這個回呼的實作會根據目前的影格編號和動畫的選項來修改點的屬性。
init/1回呼提供預設實作只要呼叫use DancingDots.Animation,就能輕鬆把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回呼,因為它不接受任何選項。
請實作handle_frame/3回呼,它負責處理單一影格。如果影格編號是四的倍數,函式應該回傳不透明度為原本一半的點。在其他影格中,則回傳不變的點。
影格從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行為來實作一個縮放動畫。
這個動畫接受一個選項:速度。速度可以是任何數字。如果是負數,點就會縮小而不是放大。
實作init/1回呼。它應該驗證傳入的選項是帶有:velocity鍵的關鍵字清單。速度的值必須是數字。如果它不是數字,就回傳錯誤"The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}"。
實作handle_frame/3回呼。它應該回傳一個點,其半徑增加(目前影格編號減一)乘以速度的量。
影格從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}