Exercism-এর অনুশীলনীগুলো ছোট, কৃত্রিম, আর অনেক সময় আপাতদৃষ্টিতে তুচ্ছ মনে হয়। অভিজ্ঞ ডেভেলপারদের এগুলো থেকে শেখার কিছুই নেই, এমনটা ভেবে নেওয়া সহজ। কিন্তু এই কৃত্রিম সমস্যাগুলো সমাধান করতে গিয়ে আপনি আপনার ভাষার এমন কিছু অংশ শিখতে ও কাজে লাগাতে পারেন, যা হয়তো আগে ঘাঁটেননি। এই নতুন শিক্ষা আপনাকে বাস্তব জগতের সমস্যা আরও দক্ষভাবে বা আরও সুন্দরভাবে সমাধান করতে সাহায্য করতে পারে।
প্যারালেল লেটার ফ্রিকোয়েন্সি হলো Exercism-এর এলিক্সির ট্র্যাক-এর একটি মধ্যম কঠিনতার অনুশীলনী, যা থেকে বিস্ময়করভাবে অনেক আকর্ষণীয় শিক্ষা পাওয়া যায়। এই অনুশীলনীটি সমাধানের একটি কেন্দ্রীয় চ্যালেঞ্জ হলো একাধিক ভাষার অক্ষর সামলানো, কারণ টেস্ট কেসগুলোর একটি জার্মান ভাষায় লেখা এবং তাতে ইংরেজি বর্ণমালার বাইরের ক্যারেক্টার আছে। আপনি যদি বেশিরভাগ সময় ইংরেজিভাষী ব্যবহারকারীদের জন্য অ্যাপ্লিকেশন তৈরি করেই কাটান, তবে এমন চাহিদা সামলাতে হতে পারে এটি আপনার প্রথমবার। এই অনুশীলনী থেকে পাওয়া শিক্ষার স্পষ্ট উপকার আছে যে কেউ বহুভাষিক বা অ-ইংরেজি অ্যাপ্লিকেশন লিখছে তার জন্য, তবে এটি আরও অনেক ক্ষেত্রেও সাহায্য করতে পারে, যেমন আরও নির্ভরযোগ্য ইউজারনেম ও পাসওয়ার্ড ভ্যালিডেশন।
অনুশীলনীটি সফলভাবে সমাধান করতে, আপনাকে একটি ফাংশন Frequency.frequency/2 তৈরি করতে হবে, যা এমন একটি স্ট্রিংয়ের অ্যারেতে অক্ষরের ফ্রিকোয়েন্সি নির্ধারণ করে, যা যেকোনো ভাষার হতে পারে:
iex> Frequency.frequency(["Freude", "schöner", "Götterfunken"], workers)
%{
"c" => 1,
"d" => 1,
"e" => 5,
...
"ö" => 2
}
চলুন, এই ফাংশনকে যে মৌলিক সমস্যাটি সমাধান করতে হবে সেটি দিয়ে শুরু করি, তারপর ধীরে ধীরে পূর্ণ ইমপ্লিমেন্টেশনে পৌঁছাই।
এলিক্সিরে একটি ক্যারেক্টার অক্ষর কি না তা নির্ধারণ
"a" একটি অক্ষর কি না তা নির্ধারণ করতে আপনি এলিক্সির কীভাবে ব্যবহার করবেন?
আমার মনে হয় বেশিরভাগ মানুষ /[a-z]/-এর মতো একটি রেগুলার এক্সপ্রেশন ব্যবহার করবেন:
iex> String.match?("a", ~r/^[a-z]$/)
true
"A"-এর ক্ষেত্রে?
i (কেস-নিরপেক্ষ) মডিফায়ার যোগ করাই সম্ভবত সবচেয়ে সহজ উপায়:
iex> String.match?("A", ~r/^[a-z]$/i)
true
ঠিক আছে, এখন "ö"-এর ক্ষেত্রে?
যখন আমি প্রথমে এই সমস্যার কাছে গিয়েছিলাম, তখন সেরা উপায়টি নিয়ে নিশ্চিত ছিলাম না। /[a-z]/i যে কাজ করবে না, সেটা নিশ্চিত:
iex> String.match?("ö", ~r/^[a-z]$/i)
false
"ö" অক্ষর কি না তা নির্ধারণ করা এই Exercism সমস্যাটি সমাধানের একটি মূল অংশ, কারণ টেস্টে থাকা লেখাগুলোর একটি জার্মান ভাষায়:
# parallel_letter_frequency_test.exs
...
# Poem by Friedrich Schiller. The corresponding music is the European Anthem.
@ode_an_die_freude """
Freude schöner Götterfunken
...
"""
হয়তো আপনি একটি রেগুলার এক্সপ্রেশন দিয়ে দেখতে পারেন যে কোনো ক্যারেক্টার বিশেষ ক্যারেক্টার নয় কি না, কিন্তু সেটি সম্ভবত দীর্ঘ, কুৎসিত আর ভঙ্গুর হবে। আপনার ফাংশনে ইনপুট হিসেবে আসতে পারে এমন প্রতিটি সম্ভাব্য বিশেষ ক্যারেক্টার আপনি ধরতে পেরেছেন, সে বিষয়ে কতটা নিশ্চিত থাকতে পারবেন? আমার মনে হয় এর চেয়ে ভালো একটি উপায় আছে।
এলিক্সিরে ইউনিকোড রেগুলার এক্সপ্রেশন
এই সমস্যার একটি ভালো উপায় হলো এলিক্সিরের Regex মডিউলের u মডিফায়ার ব্যবহার করা:
unicode (
u) -\p-এর মতো ইউনিকোড-নির্দিষ্ট প্যাটার্ন সক্রিয় করে এবং\w,\W,\sও এদের সঙ্গীদের মতো মডিফায়ার বদলে দেয়, যাতে সেগুলোও ইউনিকোডে মিলে।
দেখা যাচ্ছে, u মডিফায়ার, বিশেষত \p প্যাটার্নটি, সত্যিই একটি চমৎকার সমাধান। \p প্যাটার্নটি দিয়ে আপনি ইউনিকোড ক্যারেক্টার ক্যাটেগরিগুলোর যেকোনোটিতে একটি গ্রাফিম (একটি একক ইউনিকোড ক্যারেক্টারের অপর নাম) মেলাতে পারেন। এতে শুধু Ll (অক্ষর, ছোট হাতের) আর Sc (সিম্বল, কারেন্সি)-এর মতো নির্দিষ্ট ক্যাটেগরি নয়, L (অক্ষর) আর S (সিম্বল)-এর মতো মূল ক্যাটেগরিগুলোও অন্তর্ভুক্ত।
\p{L} প্যাটার্ন দিয়ে আপনি ইউনিকোডে অন্তর্ভুক্ত যেকোনো মানুষের ভাষার যেকোনো কেসের যেকোনো অক্ষর মেলাতে পারেন। এতে বেশ শক্তিশালী ম্যাচিং সম্ভব হয়।
ইংরেজির বেসিক ল্যাটিন ক্যারেক্টার যথারীতি কাজ করে:
iex> String.match?("a", ~r/^\p{L}$/u)
true
iex> String.match?("A", ~r/^\p{L}$/u)
true
উমলাউট আর অ্যাকিউট অ্যাকসেন্ট-সহ ল্যাটিন ক্যারেক্টারের রূপভেদও কোনো সমস্যা নয়:
iex> String.match?("ö", ~r/^\p{L}$/u)
true
iex> String.match?("Á", ~r/^\p{L}$/u)
true
চলুন নিশ্চিত হই যে এটি যেকোনো ক্যারেক্টারের জন্যই ম্যাচ ফেরত দিচ্ছে না। অক্ষরের মতো দেখতে কিন্তু অক্ষর নয়, এমন কিছু ক্যারেক্টার কেমন:
iex> String.match?("$", ~r/^\p{L}$/u)
false
iex> String.match?("@", ~r/^\p{L}$/u)
false
খুব সুন্দর, কিন্তু মনে আছে আমি যেকোনো ভাষা বলেছিলাম? কোনো সমস্যাই নেই:
# Chinese character for "you"
iex> String.match?("你", ~r/^\p{L}$/u)
true
# Cyrillic capital letter "zhe"
iex> String.match?("Ж", ~r/^\p{L}$/u)
true
আলোচ্য সমস্যায় ইউনিকোড ম্যাচিং প্রয়োগ করা
এখন যেহেতু এমন একটি টুল আমাদের হাতে আছে যা একটি গ্রাফিম অক্ষর কি না তা নির্ধারণে সাহায্য করতে পারে, আমরা সেটি কাজে লাগিয়ে সমস্যাটি সমাধান করতে পারি। Frequency.frequency/2 ফাংশনের একটি প্রাথমিক ইমপ্লিমেন্টেশন দেখতে এমন হতে পারে:
def frequency(texts, _workers) do
texts
|> get_all_graphemes()
|> count_letters()
end
defp get_all_graphemes(texts) do
texts
|> Enum.join()
|> String.graphemes()
end
count_letters/1-কে শুধু উপরে চিহ্নিত String.match?(grapheme, ~r/^\p{L}$/u) প্যাটার্নটি প্রয়োগ করে graphemes-এর অ্যারেতে প্রতিটি অক্ষরের গণনা বাড়াতে হবে। এই Exercism সমস্যার আমার সমাধান থেকে নেওয়া একটি উদাহরণ ইমপ্লিমেন্টেশন এখানে:
defp count_letters(graphemes) do
Enum.reduce(graphemes, %{}, fn grapheme, acc ->
if String.match?(grapheme, ~r/^\p{L}$/u) do
downcased_letter = String.downcase(grapheme)
Map.update(acc, downcased_letter, 1, fn count -> count + 1 end)
else
acc
end
end)
end
এই ফাংশনটি গ্রাফিমের একটি অ্যারে নেয়, যেমন ["a", "A", "ö", "$"], আর এমন একটি ম্যাপ ফেরত দেয় যা কেস উপেক্ষা করে শুধু অক্ষরগুলো গণনা করে, %{"a" => 2, "ö" => 1}। এই ফাংশন যেকোনো ভাষার ইনপুট সামলাতে পারে, সেটা ভাবলে বলতেই হয়, মাত্র ৯ লাইনের কোড হিসেবে এটি বেশ শক্তিশালী।
উপসংহার
দেখা যায়, ইউনিকোড ম্যাচিং সম্পর্কে জানলে অ-ইংরেজি অক্ষর মেলানো বেশ সহজ হয়ে যায়, আর আমাদের ভাগ্য ভালো যে এটি এলিক্সিরের Regex মডিউলের একটি মূল ফিচার। এই Exercism সমস্যাটি সমাধান করার আগে আমি এই ফিচারটি সম্পর্কে খুব একটা জানতাম না, কিন্তু এখন আমি এটিকে আমার এলিক্সির টুলবক্সের একটি অপরিহার্য অংশ বলে মনে করি।
এই নতুন টুলটি আপনি নানা ভাবে কাজে লাগাতে পারেন, আর আমার মনে যেগুলো প্রথমে আসে সেগুলো হলো পাসওয়ার্ড ও ইউজারনেমের আরও নির্ভরযোগ্য ভ্যালিডেশন, কিংবা ম্যানুয়ালি সব সম্ভাব্য কারেন্সি সিম্বল তালিকাভুক্ত না করেই কোনো ইনপুট স্ট্রিং বৈধ কারেন্সি স্ট্রিং কি না তা নির্ধারণ:
iex> currency_string_regex = ~r/\p{Sc}\d+\.\d{2}/u
~r/\p{Sc}\d+\.\d{2}/u
iex> ["$1.00", "£1.00", "¥1.00", "€1.00", "&1.00"] \
...> |> Enum.filter(&String.match?(&1, currency_string_regex))
["$1.00", "£1.00", "¥1.00", "€1.00"]