يتيح لنا الماكرو 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.
تواصل معك صديقك، وهو فنان طموح، بفكرة مشروع. لنجمع بين خياله البصري وخبرتك التقنية. حان وقت التجربة في الفن التوليدي!
القيود تساعد على الإبداع وتقصّر مواعيد تسليم المشاريع، لذا اتفقتما على حصر تحفتكما الفنية في شكل واحد فقط، وهو الدائرة. لكن سيكون هناك دوائر كثيرة، ويمكنها أن تتحرك! ستسمّيه... النقاط الراقصة.
سيرغب صديقك بالتأكيد في ابتكار حركات جديدة ومتقنة للنقاط، لذا ستبدأ البرمجة ببناء معمارية تتيح لك لاحقًا تعريف تحريكات جديدة بسهولة.
تحتاج كل وحدة تحريك إلى تنفيذ دالتَي استدعاء راجعتين: init/1 وhandle_frame/3. عرّفهما في وحدة Animation.
عرّف دالة الاستدعاء الراجعة init/1. يجب أن تأخذ وسيطًا واحدًا من النوع opts وأن تُرجع إما الصف {:ok, opts} أو الصف {:error, error}. ستتحقق تطبيقات دالة الاستدعاء هذه من صلاحية الخيارات المعطاة لهذا النوع تحديدًا من التحريك.
عرّف دالة الاستدعاء الراجعة handle_frame/3. يجب أن تأخذ ثلاثة وسائط: النقطة، ورقم الإطار، والخيارات. ويجب أن تُرجع نقطة دائمًا. ستعدّل تطبيقات دالة الاستدعاء هذه خصائص النقطة بناءً على رقم الإطار الحالي وخيارات التحريك.
init/1
ينبغي أن يكون سلوك Animation سهل الإدماج في وحدات أخرى عبر استدعاء use DancingDots.Animation.
لتحقيق ذلك، نفّذ الماكرو __using__ في وحدة Animation بحيث يضبط وحدة 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}
سجّل في Exercism لتتعلّم وتتقن Elixir عبر 58 مفهومًا168 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.