نقاط راقصة

نقاط راقصة

تمرين تعلّمي

مقدمة

الاستخدام

يتيح لنا الماكرو 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. وإذا لم تُمرَّر صراحةً، فإنها تكون افتراضيًا قائمة فارغة.

السلوكيات

تتيح لنا السلوكيات تعريف واجهات (مجموعات من الدوال والماكروات) في وحدة سلوك يمكن أن تنفّذها لاحقًا وحدات رد اتصال مختلفة. وبفضل الواجهة المشتركة، يمكن استخدام وحدات رد الاتصال هذه بالتبادل.

Note

لاحظ التهجئة البريطانية للكلمة "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.

التعليمات

تواصل معك صديقك، وهو فنان طموح، بفكرة مشروع. لنجمع بين خياله البصري وخبرتك التقنية. حان وقت التجربة في الفن التوليدي!

القيود تساعد على الإبداع وتقصّر مواعيد تسليم المشاريع، لذا اتفقتما على حصر تحفتكما الفنية في شكل واحد فقط، وهو الدائرة. لكن سيكون هناك دوائر كثيرة، ويمكنها أن تتحرك! ستسمّيه... النقاط الراقصة.

سيرغب صديقك بالتأكيد في ابتكار حركات جديدة ومتقنة للنقاط، لذا ستبدأ البرمجة ببناء معمارية تتيح لك لاحقًا تعريف تحريكات جديدة بسهولة.

1. عرّف سلوك التحريك

تحتاج كل وحدة تحريك إلى تنفيذ دالتَي استدعاء راجعتين: init/1 وhandle_frame/3. عرّفهما في وحدة Animation.

عرّف دالة الاستدعاء الراجعة init/1. يجب أن تأخذ وسيطًا واحدًا من النوع opts وأن تُرجع إما الصف {:ok, opts} أو الصف {:error, error}. ستتحقق تطبيقات دالة الاستدعاء هذه من صلاحية الخيارات المعطاة لهذا النوع تحديدًا من التحريك.

عرّف دالة الاستدعاء الراجعة handle_frame/3. يجب أن تأخذ ثلاثة وسائط: النقطة، ورقم الإطار، والخيارات. ويجب أن تُرجع نقطة دائمًا. ستعدّل تطبيقات دالة الاستدعاء هذه خصائص النقطة بناءً على رقم الإطار الحالي وخيارات التحريك.

2. وفّر تنفيذًا افتراضيًا لدالة الاستدعاء الراجعة 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]}

3. نفّذ تحريك 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}

4. نفّذ تحريك 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}
تعديل عبر GitHub يفتح الرابط في نافذة أو علامة تبويب جديدة
Elixir Exercism

مستعد لبدء نقاط راقصة؟

سجّل في Exercism لتتعلّم وتتقن Elixir عبر 58 مفهومًا168 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.