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 ম্যাক্রো কল করুন। এটি কী (key) হিসেবে ফাংশনের নাম আর মান হিসেবে ফাংশনের অ্যারিটি নিয়ে একটি কিওয়ার্ড লিস্ট গ্রহণ করে।
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 ম্যাক্রোতে সেগুলো ইমপোর্ট করতে পারেন।
আপনার বন্ধু, একজন উচ্চাকাঙ্ক্ষী শিল্পী, একটি প্রজেক্টের আইডিয়া নিয়ে আপনার কাছে এসেছেন। চলুন তাঁর ভিজ্যুয়াল সৃজনশীলতার সাথে আপনার টেকনিক্যাল দক্ষতা মিলিয়ে দিই। এখন জেনারেটিভ আর্ট নিয়ে একটু হাত পাকানোর সময় এসেছে!
সীমাবদ্ধতা সৃজনশীলতাকে সাহায্য করে আর প্রজেক্টের সময়সীমা কমিয়ে দেয়, তাই你们两个 — no. আপনি দুজনে মিলে আপনার মাস্টারপিসটিকে একটি মাত্র আকৃতিতে সীমিত রাখতে রাজি হয়েছেন, আর সেই আকৃতিটি হলো বৃত্ত। কিন্তু বৃত্ত থাকবে অনেক। আর তারা এদিক-ওদিক চলাফেরাও করতে পারবে! এর নাম হবে... ড্যান্সিং ডটস।
আপনার বন্ধু নিশ্চিতভাবেই ডটগুলোর জন্য নতুন নতুন জটিল মুভমেন্ট বানাতে চাইবেন, তাই আপনি কোডিং শুরু করবেন এমন একটি আর্কিটেকচার তৈরি করে, যা দিয়ে পরে সহজেই নতুন অ্যানিমেশন ডিফাইন করা যাবে।
প্রতিটি অ্যানিমেশন মডিউলকে দুটি কলব্যাক ইমপ্লিমেন্ট করতে হবে: 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 অ্যানিমেশনটি ইমপ্লিমেন্ট করুনএই অ্যানিমেশনটি একটি অপশন নেয়: ভেলোসিটি। ভেলোসিটি যেকোনো সংখ্যা হতে পারে। এটি ঋণাত্মক হলে ডটটি জুম-ইনের বদলে জুম-আউট হয়।
init/1 কলব্যাকটি ইমপ্লিমেন্ট করুন। এটি যাচাই করবে যে পাঠানো অপশনগুলো :velocity কী (key) সহ একটি কিওয়ার্ড লিস্ট। ভেলোসিটির মান অবশ্যই একটি সংখ্যা হতে হবে। এটি সংখ্যা না হলে এই এররটি রিটার্ন করুন: "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টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।