যেকোনো ভাষাতেই বাইনারি ডেটা নিয়ে কাজ করা একটি গুরুত্বপূর্ণ ধারণা, আর Elixir বাইনারি ডেটা লেখা, মেলানো ও গঠন করার জন্য একটি সুন্দর সিনট্যাক্স দেয়।
Elixir-এ বাইনারি ডেটাকে বিটস্ট্রিং টাইপ বলা হয়। বাইনারি ডেটা টাইপ (সাধারণ বাইনারি ডেটার সাথে গুলিয়ে ফেলা যাবে না) হলো বিটস্ট্রিংয়ের একটি নির্দিষ্ট রূপ, যা নিয়ে আমরা পরের একটি অনুশীলনীতে আলোচনা করব।
বিটস্ট্রিং লিটারাল বিটস্ট্রিং স্পেশাল ফর্ম <<>> ব্যবহার করে ডিফাইন করা হয়। একটি বিটস্ট্রিং লিটারাল ডিফাইন করার সময় সেটি সেগমেন্টে সেগমেন্টে ডিফাইন করা হয়। প্রতিটি সেগমেন্টের একটি মান ও একটি টাইপ থাকে, যেগুলো :: অপারেটর দিয়ে আলাদা করা হয়। টাইপটি বলে দেয় মানটি এনকোড করতে কতগুলো বিট ব্যবহার হবে। টাইপটি সম্পূর্ণভাবে বাদ দেওয়াও যায়, সেক্ষেত্রে ডিফল্ট হিসেবে ৮-বিট ইন্টিজার মান ধরা হয়।
# This defines a bitstring with three segments of a single bit each
<<0::1, 1::1, 0::1>>
টাইপ ::1 হিসেবে লেখা হলো ::size(1) লেখার একটি সংক্ষিপ্ত রূপ। বিট সাইজ যদি কোনো ভ্যারিয়েবল থেকে আসে, তাহলে আপনাকে বড় সিনট্যাক্সটি ব্যবহার করতে হবে।
বাইনারি ইন্টিজার লিটারাল লেখার সময় আমরা লিটারালের সামনে 0b বসিয়ে সরাসরি বেস-২ নোটেশনে লিখতে পারি। মনে রাখবেন, টেস্টের ফলাফলে বা iex ব্যবহার করার সময় এগুলো যেভাবেই হোক দশমিক সংখ্যা হিসেবেই দেখানো হবে।
<<0b1011::4>> == <<11::4>>
# => true
যদি সেগমেন্টের মান সেগমেন্টের টাইপের ধারণক্ষমতার চেয়ে বেশি হয়ে যায়, তাহলে এটি বাম দিক থেকে ট্রাংকেট করা হবে।
<<0b1011::3>> == <<0b0011::3>>
# => true
স্পেশাল ফর্ম ব্যবহার করে আপনি একটি বিদ্যমান বিটস্ট্রিংয়ের সামনে ও পেছনে দুটোই যোগ করতে পারেন। বিদ্যমান বিটস্ট্রিংটির সাইজ অজানা হলে সেটির জন্য ::bitstring টাইপ ব্যবহার করতে হবে।
value = <<0b110::3, 0b001::3>>
new_value = <<0b011::3, value::bitstring, 0b000::3>>
# => <<120, 8::size(4)>>
স্পেশাল ফর্ম ব্যবহার করে আমরা ভ্যারিয়েবলে রাখা বিটস্ট্রিংগুলো কনক্যাটেনেট করতে পারি। অজানা সাইজের দুটি বিটস্ট্রিং কনক্যাটেনেট করার সময় ::bitstring টাইপ ব্যবহার করতে হবে।
first = <<0b110::3>>
second = <<0b001::3>>
concatenated = <<first::bitstring, second::bitstring>>
# => <<49::size(6)>>
স্পেশাল ফর্ম থেকে মান বের করতেও প্যাটার্ন ম্যাচিং করা যায়। যে ফ্র্যাগমেন্টটি আপনি ধরতে চান, তার জন্য কত বিট লাগবে সেটি আপনাকে জানতে হবে, তবে একটি ব্যতিক্রম আছে: অজানা সাইজের বিটস্ট্রিংয়ের উপর প্যাটার্ন ম্যাচ করার জন্য ::bitstring টাইপ ব্যবহার করা যায়, কিন্তু এটি শুধু শেষ ফ্র্যাগমেন্টের জন্যই ব্যবহার করা যাবে।
<<value::4, rest::bitstring>> = <<0b01101001::8>>
value == 0b0110
# => true
বিটস্ট্রিংগুলো হয়তো (টেস্ট রানার বা iex-এ) যে ফরম্যাটে তৈরি করা হয়েছিল তার চেয়ে ভিন্ন ফরম্যাটে প্রিন্ট হতে পারে। বিটস্ট্রিং শেখার সময় এটি প্রায়ই বিভ্রান্তির সৃষ্টি করে।
ডিফল্টভাবে বিটস্ট্রিংগুলো ৮ বিটের (এক বাইট) ফ্র্যাগমেন্টে দেখানো হয়, এমনকি আপনি যদি সেগুলো ভিন্ন সাইজের ফ্র্যাগমেন্টে তৈরি করে থাকেন।
<<2011::11>>
# => <<251, 3::size(3)>>
আপনি যদি এমন একটি বিটস্ট্রিং তৈরি করেন যা একটি প্রিন্টযোগ্য UTF-8 এনকোডেড স্ট্রিং প্রকাশ করে, তাহলে সেটি স্ট্রিং হিসেবে দেখানো হয়।
<<>>
# => ""
<<65, 66, 67>>
# => "ABC"
পেইন্ট বাই নাম্বার (যা কালার বাই নাম্বার নামেও পরিচিত) হলো রং করার জন্য তৈরি সাদা-কালো ছবি। ছবির বিভিন্ন অংশে ভিন্ন ভিন্ন সংখ্যা লেখা থাকে, আর সেই সংখ্যাগুলো আগে থেকে ঠিক করা একটি রঙের প্যালেটের নির্দিষ্ট রঙের সঙ্গে মেলে। লক্ষ্য হলো সঠিক রং দিয়ে অংশগুলো ভরা, যাতে শেষে একটি সুন্দর রঙিন ছবি ফুটে ওঠে। এটি শিশু আর বড় সবার জন্যই একটি আরামদায়ক কাজ।
আপনার কাজ হলো Elixir-এ একটি পেইন্ট বাই নাম্বার অ্যাপ লেখা। আপনি চান আপনার অ্যাপ যেন একটি কাস্টম ডেটা ফরম্যাটে ছবি ইমপোর্ট ও এক্সপোর্ট করতে পারে। আপনার ছবির ডেটা সংরক্ষণ করতে আপনি বাইনারি ফাইল ব্যবহার করার সিদ্ধান্ত নিয়েছেন।
এই অনুশীলনীটি ধরে নেয় যে আপনি বাইনারি সংখ্যা সম্পর্কে জানেন এবং বাইনারি সংখ্যাকে দশমিক সংখ্যায় আর দশমিক সংখ্যাকে বাইনারি সংখ্যায় বদলানোর পেছনের নীতিগুলো বোঝেন।
চলুন কল্পনা করি আপনার কাছে নিচে দেখানো মতো একটি স্মাইলির ছবি আছে। ছবিটির পটভূমি সাদা। স্মাইলিটির সীমানা কালো আর ভরাট রং হলুদ।
এই ছবিটিতে ৩টি রং ব্যবহৃত হয়েছে। ধরা যাক আমরা সেই রঙগুলোর জন্য ইনডেক্স ঠিক করি:
0 (বাইনারি: 0b00) সাদার জন্য,1 (বাইনারি: 0b01) কালোর জন্য,2 (বাইনারি: 0b10) হলুদের জন্য।এখন আমরা প্রতিটি পিক্সেলের রং প্রকাশ করতে ওই রঙের ইনডেক্সগুলো ব্যবহার করতে পারি।
| স্মাইলি | রঙের ইনডেক্সসহ স্মাইলি |
|---|---|
![]() |
![]() |
PaintByNumber.palette_bit_size/1 ফাংশনটি ইমপ্লিমেন্ট করুন। এটি প্যালেটের রঙের সংখ্যা নেবে এবং সেই সংখ্যক রঙের ইনডেক্সকে বাইনারি সংখ্যা হিসেবে প্রকাশ করতে কতটি বিট দরকার, তা রিটার্ন করবে। রঙের ইনডেক্স সবসময় 0 থেকে শুরু হয় এবং পরপর বাড়তে থাকা ইন্টিজার হয়।
যেমন, ১৩টি ভিন্ন রং প্রকাশ করতে ৪টি বিট দরকার। ৪টি বিট দিয়ে সর্বোচ্চ ১৬টি রঙের ইনডেক্স রাখা যায় (2^4)। ৩টি বিট যথেষ্ট হবে না, কারণ ৩টি বিট দিয়ে সর্বোচ্চ ৮টি রঙের ইনডেক্স রাখা যায় (2^3)।
PaintByNumber.palette_bit_size(13)
# => 4
দ্রষ্টব্য: Elixir স্ট্যান্ডার্ড লাইব্রেরিতে কোনো log2 ফাংশন নেই। পরে আপনি শিখবেন কীভাবে Elixir থেকে Erlang লাইব্রেরি ব্যবহার করতে হয়, যেখানে এই ফাংশনটি পাবেন। এখন বরং রিকার্সন আর পাওয়ার অপারেটর ** দিয়ে এই কাজটি সমাধান করুন।
PaintByNumber.empty_picture/0 ফাংশনটি ইমপ্লিমেন্ট করুন। এটি একটি খালি বিটস্ট্রিং রিটার্ন করবে।
আপনার অ্যাপের মৌলিক ফিচারগুলো হাতে-কলমে পরীক্ষা করার জন্য একটি আগে থেকে নির্ধারিত টেস্ট ছবি ব্যবহার করা হবে। টেস্ট ছবিটি ৪টি ভিন্ন রঙের ৪টি পিক্সেল নিয়ে গঠিত।
PaintByNumber.test_picture/0 ফাংশনটি ইমপ্লিমেন্ট করুন। এটি এমন একটি বিটস্ট্রিং রিটার্ন করবে যা ৪টি সেগমেন্ট নিয়ে গঠিত।
প্রতিটি সেগমেন্টের বিট সাইজ হবে 2। সেগমেন্টগুলোর মান হবে 0, 1, 2 আর 3।
PaintByNumber.prepend_pixel/3 ফাংশনটি ইমপ্লিমেন্ট করুন। এটি তিনটি আর্গুমেন্ট নেবে: যে ছবির শুরুতে যোগ করা হচ্ছে সেই ছবির বিটস্ট্রিং, প্যালেটের রঙের সংখ্যা, আর নতুন পিক্সেলের রঙের ইনডেক্স। এটি এমন একটি বিটস্ট্রিং রিটার্ন করবে যেখানে নতুন পিক্সেলটি ছবির শুরুতে যোগ করা হয়েছে।
picture = <<2::4, 0::4>>
color_count = 13
pixel_color_index = 11
PaintByNumber.prepend_pixel(picture, color_count, pixel_color_index)
# => <<178, 0::size(4)>>
# (which is equal to <<11::4, 2::4, 0::4>>)
PaintByNumber.get_first_pixel/2 ফাংশনটি ইমপ্লিমেন্ট করুন। এটি দুটি আর্গুমেন্ট নেবে: যে ছবিটি থেকে পড়া হচ্ছে সেই ছবির বিটস্ট্রিং, আর প্যালেটের রঙের সংখ্যা। এটি প্রদত্ত ছবির প্রথম পিক্সেলের রঙের ইনডেক্স রিটার্ন করবে। খালি ছবি দিলে এটি nil রিটার্ন করবে।
picture = <<19::5, 2::5, 18::5>>
color_count = 20
PaintByNumber.get_first_pixel(picture, color_count)
# => 19
PaintByNumber.drop_first_pixel/2 ফাংশনটি ইমপ্লিমেন্ট করুন। এটি দুটি আর্গুমেন্ট নেবে: যে ছবিটি থেকে একটি পিক্সেল সরানো হচ্ছে সেই ছবির বিটস্ট্রিং, আর প্যালেটের রঙের সংখ্যা। এটি প্রথম পিক্সেল ছাড়া ছবিটি রিটার্ন করবে। খালি ছবি দিলে এটি একটি খালি ছবি রিটার্ন করবে।
picture = <<2::3, 5::3, 5::3, 0::3>>
color_count = 6
PaintByNumber.drop_first_pixel(picture, color_count)
# => <<180, 0::size(1)>>
# (which is equal to <<5::3, 5::3, 0::3>>)
PaintByNumber.concat_pictures/2 ফাংশনটি ইমপ্লিমেন্ট করুন। এটি দুটি আর্গুমেন্ট নেবে, দুটি বিটস্ট্রিং। এটি এমন একটি বিটস্ট্রিং রিটার্ন করবে, যা প্রথম আর্গুমেন্টটি দ্বিতীয় আর্গুমেন্টের শুরুতে যোগ করার ফল।
picture1 = <<52::6, 51::6>>
picture2 = <<0::6, 34::6, 12::6>>
PaintByNumber.concat_pictures(picture1, picture2)
# => <<211, 48, 34, 12::size(6)>>
# (which is equal to <<52::6, 51::6, 0::6, 34::6, 12::6>>)
Exercism-এ সাইন আপ করুন, Elixir ট্র্যাকের 58টি কনসেপ্ট168টি অনুশীলনী আর সত্যিকারের মানুষের মেন্টরিং দিয়ে শিখুন ও দক্ষ হয়ে উঠুন, সম্পূর্ণ বিনামূল্যে।