use«ماکرو»ی use به ما اجازه میدهد که ماژول خود را بهسرعت با قابلیتهایی گسترش دهیم که ماژول دیگری فراهم میکند. وقتی ماژولی را use میکنیم، آن ماژول میتواند کدی را به ماژول ما تزریق کند؛ برای مثال میتواند توابعی تعریف کند، ماژولهای دیگر را import یا alias کند، یا ویژگیهای ماژول را تنظیم کند.
اگر تا به حال فایلهای تست برخی از تمرینهای Elixir را اینجا در Exercism نگاه کرده باشید، به احتمال زیاد متوجه شدهاید که همهی آنها با 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 استفاده میکنیم. مقدار آن باید اسم ماژول رفتاری باشد که در حال افزودن آن هستیم.
سپس باید همهی توابع (فراخوانهای بازگشتی) موردنیاز آن ماژول رفتاری را تعریف کنیم. اگر در حال پیادهسازی رفتار شخص دیگری باشیم، مانند رفتارهای داخلی Access یا GenServer در Elixir، فهرست همهی فراخوانهای بازگشتی آن رفتار را در مستندات 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 import کنید.
دوست شما، هنرمندی نوپا، با ایدهی یک پروژه به سراغتان آمد. بیایید خلاقیت بصری او را با تخصص فنی شما ترکیب کنیم. وقت آن رسیده که در هنر مولد دستی ببرید!
محدودیتها به خلاقیت کمک میکنند و مهلت پروژه را کوتاهتر میکنند، پس هر دو توافق کردهاید شاهکارتان را به یک شکل محدود کنید: دایره. اما قرار است دایرههای زیادی داشته باشیم. و میتوانند جابهجا شوند! اسمش را میگذارید... نقطههای رقصان.
دوست شما قطعاً میخواهد حرکتهای تازه و پیچیدهای برای نقطهها ابداع کند، پس کدنویسی را با ساختن معماریای شروع میکنید که بعداً به شما اجازه میدهد انیمیشنهای جدید را بهراحتی تعریف کنید.
هر ماژول انیمیشن باید دو callback را پیادهسازی کند: init/1 و handle_frame/3. آنها را در ماژول Animation تعریف کنید.
callback یعنی init/1. باید یک آرگومان از نوع opts بگیرد و یک تاپل {:ok, opts} یا تاپل {:error, error} بازگرداند. پیادهسازیهای این callback بررسی میکنند که آیا گزینههای دادهشده برای این نوع خاص از انیمیشن معتبرند یا نه.
callback یعنی handle_frame/3 را تعریف کنید. باید سه آرگومان بگیرد: نقطه، شمارهی فریم و گزینهها. باید همیشه یک نقطه بازگرداند. پیادهسازیهای این callback ویژگیهای نقطه را بر اساس شمارهی فریم جاری و گزینههای انیمیشن تغییر میدهند.
init/1 فراهم کنیدرفتار Animation باید طوری باشد که با فراخوانی use DancingDots.Animation بهراحتی در ماژولهای دیگر جای بگیرد.
برای این کار، ماکروی __using__ را در ماژول Animation پیادهسازی کنید تا ماژول Animation را بهعنوان رفتار ماژول دیگر تعیین کند. همچنین باید یک پیادهسازی پیشفرض از callback یعنی init/1 فراهم کند. پیادهسازی پیشفرض init/1 باید گزینههای دادهشده را بدون تغییر بازگرداند.
defmodule MyCustomAnimation do
use DancingDots.Animation
end
MyCustomAnimation.init(some_option: true)
# => {:ok, [some_option: true]}
Flicker را پیادهسازی کنیدبا استفاده از رفتار Animation یک انیمیشن چشمکزن پیادهسازی کنید.
باید از callback پیشفرض init/1 استفاده کند، چون هیچ گزینهای نمیگیرد.
callback یعنی 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 یک انیمیشن زوم پیادهسازی کنید.
این انیمیشن یک گزینه میگیرد: سرعت. سرعت میتواند هر عددی باشد. اگر منفی باشد، نقطه بهجای بزرگتر شدن، کوچکتر میشود.
callback یعنی init/1 را پیادهسازی کنید. باید بررسی کند که گزینههای دادهشده یک لیست کلیدواژهای با کلید :velocity هستند. مقدار سرعت باید یک عدد باشد. اگر عدد نباشد، خطای "The :velocity option is required, and its value must be a number. Got: #{inspect(velocity)}" را بازگردانید.
callback یعنی 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}