কম্পিউটার টিউটোরিয়াল

রুবি প্যাটার্ন ম্যাচিং মাস্টার: একটি প্র্যাকটিক্যাল গাইড

রুবিতে প্যাটার্ন ম্যাচিং সম্পর্কে একটি সংক্ষিপ্ত আলোচনা দিয়ে শুরু করি, এটি কী করে, এবং কীভাবে কোড রিডেবিলিটি উন্নত করতে সাহায্য করতে পারে।

আমার মতামত যদি আপনি কয়েক বছর আগে থাকেন, তবে আপনি হয়তো রেগেক্স-এ প্যাটার্ন ম্যাচিং-এর সাথে এটিকে গুলিয়ে ফেলবেন। 'প্যাটার্ন ম্যাচিং' গুগল সার্চ করলে অন্য কোনো কনটেক্সট ছাড়া আপনাকে সেটির কাছাকাছি কনটেন্টই দেবে।

ফরম্যালি, প্যাটার্ন ম্যাচিং হলো যেকোনো ডেটা (হওয়া ক্যারেক্টার সিকোয়েন্স, টোকেন সিরিজ, টাপল, বা অন্য কিছু) অন্য ডেটার বিপরীতে চেক করার প্রসেস।

প্রোগ্রামিং ভাষার ক্ষমতার ওপর নির্ভর করে, এর মানে নিচের যেকোনো কিছু হতে পারে:

  1. এক্সপেক্টেড ডেটা টাইপের বিপরীতে ম্যাচ করা
  2. এক্সপেক্টেড হ্যাশ স্ট্রাকচারের বিপরীতে ম্যাচ করা (যেমন নির্দিষ্ট কী-এর প্রেজেন্স)
  3. এক্সপেক্টেড অ্যারে লেন্থের বিপরীতে ম্যাচ করা
  4. ম্যাচগুলো (বা তাদের অংশ) কিছু ভেরিয়েবলে অ্যাসাইন করা

আমার প্যাটার্ন ম্যাচিং-এ প্রথম প্রবেশ হলো Elixir-এ। Elixir-এ প্যাটার্ন ম্যাচিং-র ফার্স্ট-ক্লাস সাপোর্ট আছে, তেমন যে = অপারেটর আসলে match অপারেটর, সিম্পল অ্যাসাইনমেন্ট নয়।

এটি মানে Elixir-এ নিচের কোড আসলে ভ্যালিড:

iex> x = 1
iex> 1 = x

এটি মাথায় রেখে, চলুন রুবি ২.৭+-এর নতুন প্যাটার্ন ম্যাচিং সাপোর্ট এবং কীভাবে আমরা এটি ব্যবহার করতে পারি আমাদের কোড আরও রিডেবল করার জন্য, আজ থেকে শুরু করি।

রুবি প্যাটার্ন ম্যাচিং case/in দিয়ে

রুবি প্যাটার্ন ম্যাচিং সাপোর্ট করে একটি স্পেশাল case/in এক্সপ্রেশন দিয়ে। সিনট্যাক্স:

case  in 
  # ...
in 
  # ...
else
  # ...
end

এটিকে case/when এক্সপ্রেশনের সাথে গুলিয়ে ফেলবেন না। when এবং in ব্রাঞ্চ একটি সিঙ্গল case-এ মিক্স করা যায় না।

যদি আপনি else এক্সপ্রেশন দেন না, কোনো ফেইলড ম্যাচ NoMatchingPatternError রেইজ করবে।

রুবিতে অ্যারে প্যাটার্ন ম্যাচিং

প্যাটার্ন ম্যাচিং ব্যবহার করা যায় অ্যারে কে প্রি-রিকোয়ায়ার্ড স্ট্রাকচার, ডেটা টাইপ, লেন্থ বা ভ্যালুর বিপরীতে ম্যাচ করতে।

উদাহরণস্বরূপ, নিচের সবগুলো ম্যাচ (নোট করুন শুধু প্রথম in ইভ্যালুয়েট হবে, কারণ case প্রথম ম্যাচ পাওয়ার পর খোঁজা বন্ধ করে দেয়):

case [1, 2, "Three"]
in [Integer, Integer, String]
  "matches"
in [1, 2, "Three"]
  "matches"
in [Integer, *]
  "matches"
in [a, *]
  "matches"
  # এবং ভেরিয়েবল a-এর ভ্যালু এখন ১
end

এই টাইপের প্যাটার্ন ম্যাচিং ক্লজ খুব উপকারী যখন আপনি একটি মেথড কল থেকে একাধিক সিগন্যাল প্রোডিউস করতে চান।

Elixir-ওয়ার্ল্ডে, এটি প্রায়শই ব্যবহার হয় যখন অপারেশন করা হয় যেগুলোর দুটি ফলাফল হতে পারে: :ok রেজাল্ট এবং :error রেজাল্ট, উদাহরণস্বরূপ, ডাটাবেসে ইনসার্ট করা।

এখানে কীভাবে আমরা এটি ব্যবহার করতে পারি বেটার রিডেবিলিটির জন্য:

def create
  case save(model_params)
  in [:ok, model]
    render json: => model
  in [:error, errors]
    render json: => errors
  end
end

# আপনার কোডে কোথাও, উদাহরণস্বরূপ, একটি গ্লোবাল হেল্পার বা মডেল বেস ক্লাসে (ভিন্ন নামে)
def save(attrs)
  model = Model.new(attrs)
  model.save ? [:ok, model] : [:error, model.errors]
end

রুবিতে অবজেক্ট প্যাটার্ন ম্যাচিং

আপনি রুবিতে অবজেক্টও ম্যাচ করতে পারেন একটি নির্দিষ্ট স্ট্রাকচার এনফোর্স করতে:

case {a: 1, b: 2}
in {a: Integer}
  "matches"
in {a: Integer, **}
  "matches"
in {a: a}
  "matches"
  # এবং ভেরিয়েবল a-এর ভ্যালু এখন ১
in {a: Integer => a}
  "matches"
  # এবং ভেরিয়েবল a-এর ভ্যালু এখন ১
in {a: 1, b: b}
  "matches"
  # এবং ভেরিয়েবল b-এর ভ্যালু এখন ২
in {a: Integer, **nil}
  "does not match"
  # এটি শুধু ম্যাচ করবে যদি অবজেক্টে a থাকে এবং অন্য কোনো কী নেই
end

এটি খুব ভালো কাজ করে যেকোনো প্যারামসের বিপরীতে স্ট্রিক রুল আরোপ করার জন্য।

উদাহরণস্বরূপ, আপনি একটি ফ্যান্সি গ্রিটার লিখছেন, সেটির নিচের (স্ট্রিক্টলি opiniónated) স্ট্রাকচার হতে পারে:

def greet(hash = {})
  case hash
  in {greeting: greeting, first_name: first_name, last_name: last_name}
    greet(greeting: greeting, name: "#{first_name} #{last_name}")
  in {greeting: greeting, name: name}
    puts "#{greeting}, #{name}"
  in {name: name}
    greet(greeting: "Hello", name: name)
  in {greeting: greeting}
    greet(greeting: greeting, name: "Anonymous")
  else
    greet(greeting: "Hello", name: "Anonymous")
  end
end

greet
# Hello, Anonymous
greet(name: "John")
# Hello, John
greet(first_name: "John", last_name: "Doe")
# Hello, John Doe
greet(greeting: "Bonjour", first_name: "John", last_name: "Doe")
# Bonjour, John Doe
greet(greeting: "Bonjour")
# Bonjour, Anonymous

রুবিতে ভেরিয়েবল বাইন্ডিং এবং পিনিং

উপরের কিছু উদাহরণে আমরা দেখেছি, প্যাটার্ন ম্যাচিং প্যাটার্নের অংশ যেকোনো আর্বিট্রারি ভেরিয়েবলে অ্যাসাইন করার জন্য খুব উপকারী। এটিকে ভেরিয়েবল বাইন্ডিং বলা হয়, এবং এক ভেরিয়েবলে বাইন্ড করার কয়েকটি পথ আছে:

  1. স্ট্রিক টাইপ ম্যাচ দিয়ে, যেমন in [Integer => a] বা in {a: Integer => a}
  2. টাইপ স্পেসিফিকেশন ছাড়া, যেমন in [a, 1, 2] বা in {a: a}।
  3. ভেরিয়েবল নাম ছাড়া, যা ডিফল্টভাবে কী নাম ব্যবহার করে, যেমন in {a:} একটি ভেরিয়েবল ডিফাইন করবে a নামে কী a-এর ভ্যালু দিয়ে।
  4. রেস্ট বাইন্ড, যেমন in [Integer, *rest] বা in {a: Integer, **rest}।

তবে, কীভাবে ম্যাচ করব যখন আমরা একটি এক্সিস্টিং ভেরিয়েবল সাব-প্যাটার্ন হিসেবে ব্যবহার করতে চাই? এই সময় আমরা ভেরিয়েবল পিনিং ব্যবহার করতে পারি ^ (পিন) অপারেটর দিয়ে:

a = 1
case {a: 1, b: 2}
in {a: ^a}
  "matches"
end

আপনি এমনকি এটি ব্যবহার করতে পারেন যখন একটি ভেরিয়েবল একটি প্যাটার্নে ডিফাইন করা থাকে, যা আপনাকে এমন পাওয়ারফুল প্যাটার্ন লিখতে দেয়:

case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
  puts "both billing and shipping are to the same city"
else
  raise "both billing and shipping must be to the same city"
end

ভেরিয়েবল বাইন্ডিং-এ একটি গুরুত্বপূর্ণ ক্যুয়ার্ক 언급 করা দরকার: এমনকি যদি প্যাটার্ন ফুলি ম্যাচ না করে, ভেরিয়েবলটি যাই হোক বাইন্ড হয়ে যাবে। এটি কখনও কখনও উপকারী হতে পারে।

কিন্তু, অধিকাংশ কেসে, এটি সাবটল বাগের কারণও হতে পারে — তাই নিশ্চিত করুন আপনি শ্যাডোড ভেরিয়েবল ভ্যালুতে নির্ভর করছেন না যা ম্যাচের ভিতরে ব্যবহার করা হয়েছে। উদাহরণস্বরূপ, নিচের কোডে, আপনি আশা করবেন সিটি হবে "Amsterdam", কিন্তু সেটি হবে "Berlin":

city = "Amsterdam"
order = {billing_address: {city: "Berlin"}, shipping_address: {city: "Zurich"}}
case order
in {billing_address: {city:}, shipping_address: {city: ^city}}
  puts "both billing and shipping are to the same city"
else
  puts "both billing and shipping must be to the same city"
end
puts city # Berlin instead of Amsterdam

রুবির কাস্টম ক্লাস ম্যাচ করা

আপনি কিছু স্পেশাল মেথড ইমপ্লিমেন্ট করতে পারেন কাস্টম ক্লাসকে রুবিতে প্যাটার্ন ম্যাচিং অ্যোয়্যার করার জন্য।

উদাহরণস্বরূপ, একটি ইউজারকে তার first_name এবং last_name অনুযায়ী প্যাটার্ন ম্যাচ করতে, আমরা ক্লাসে deconstruct_keys ডিফাইন করতে পারি:

class User
  def deconstruct_keys(keys)
    {first_name: first_name, last_name: last_name}
  end
end

case user
in {first_name: "John"}
  puts "Hey, John"
end

deconstruct_keys-এর keys আর্গুমেন্টে রিকোয়েস্ট করা কীগুলো থাকে প্যাটার্নে। এটি রিসিভারের জন্য একটি উপায় শুধু রিকোয়েস্ট করা কীগুলো প্রদান করার যদি সকল কম্পিউট করা এক্সপেনসিভ হয়।

একইভাবে deconstruct_keys-এর মত, আমরা deconstruct ইমপ্লিমেন্টেশন প্রদান করতে পারি অবজেক্টকে অ্যারে হিসেবে প্যাটার্ন ম্যাচ করার জন্য। উদাহরণস্বরূপ, ধরি আমাদের একটি Location ক্লাস আছে যাতে ল্যাটিটিউড এবং লংগিটিউড আছে। deconstruct_keys দিয়ে ল্যাটিটিউড ও লংগিটিউড কী প্রদান করার পাশাপাশি, আমরা একটি অ্যারে এক্সপোজ করতে পারি [latitude, longitude] ফরম্যাটে:

class Location
  def deconstruct
    [latitude, longitude]
  end
end

case location
in [Float => latitude, Float => longitude]
  puts "#{latitude}, #{longitude}"
end

কমপ্লেক্স প্যাটার্নের জন্য গার্ডস ব্যবহার

যদি আমাদের কমপ্লেক্স প্যাটার্ন থাকে যেগুলো রেগুলার প্যাটার্ন ম্যাচ অপারেটর দিয়ে রিপ্রেজেন্ট করা যায় না, তবে আমরা একটি if (বা unless) স্টেটমেন্ট ব্যবহার করতে পারেন ম্যাচের জন্য একটি গার্ড প্রদান করতে:

case [1, 2]
in [a, b] if b == a * 2
  "matches"
else
  "no match"
end

case ছাড়া =>/in দিয়ে প্যাটার্ন ম্যাচিং

আপনি যদি রুবি ৩+ ব্যবহার করেন, আরও প্যাটার্ন ম্যাচিং ম্যাজিক পাওয়া যাবে। রুবি ৩ থেকে, প্যাটার্ন ম্যাচিং একটি সিঙ্গেল লাইনে করা যায় case স্টেটমেন্ট ছাড়া:

[1, 2, "Three"] => [Integer => one, two, String => three]
puts one # 1
puts two # 2
puts three # Three
# উপরের সেম
[1, 2, "Three"] in [Integer => one, two, String => three]

যেহেতু উপরের সিনট্যাক্সে else ক্লজ নেই, এটি সবচেয়ে উপকারী যখন ডেটা স্ট্রাকচার আগে থেকে জানা থাকে।

উদাহরণস্বরূপ, এই প্যাটার্ন একটি বেস কন্ট্রোলারে খুব ফিট হয় যেটি শুধু অ্যাডমিন ইউজার আলাউ করে:

class AdminController < AuthenticatedController
  before_action :verify_admin
  private
  def verify_admin
    Current.user => {role: :admin}
    rescue NoMatchingPatternError
    raise NotAllowedError
  end
end

রুবিতে প্যাটার্ন ম্যাচিং: এই স্পেসটি ওয়াচ করুন

প্রথম প্রথম, প্যাটার্ন ম্যাচিং কিছুটা স্ট্রেঞ্জ লাগতে পারে বুঝতে। কএকজন জন্য, এটি গ্লোরিফাইড অবজেক্ট/অ্যারে ডিকন্সট্রাকশনের মতো লাগতে পারে।

কিন্তু যদি Elixir-এর পপুলারিটি কোনো ইন্ডিকেশন হয়, প্যাটার্ন ম্যাচিং আপনার আর্মেন্টের একটি চমৎকার টুল। ফার্স্ট-হ্যান্ড এক্সপেরিয়েন্স দিয়ে এটি Elixir-এ ব্যবহার করলে, আমি কনফার্ম করতে পারি একবার অভ্যাস হলে এর ছাড়া থাকা কঠিন।

আপনি যদি রুবি ২.৭ ব্যবহার করেন, প্যাটার্ন ম্যাচিং (case/in) এখনো এক্সপেরিমেন্টাল। রুবি ৩-এ, case/in স্টেবল হয়েছে এবং নতুন সিঙ্গেল-লাইন প্যাটার্ন ম্যাচিং এক্সপ্রেশন এক্সপেরিমেন্টাল। ওয়ার্নিং অফ করা যায় Warning[:experimental] = false কোডে বা -W:no-experimental কমান্ড-লাইন কী দিয়ে।

যদিও রুবিতে প্যাটার্ন ম্যাচিং এখনো আrali স্টেজে, আশা করি এই ইন্ট্রোডাকশন আপনাকে উপকারী লেগেছে এবং আপনি ভবিষ্যতের ডেভেলপমেন্টের জন্য আমার মত চমৎকার হয়েছেন!

P.S. Ruby Magic পোস্টগুলো প্রেস থেকে বের होतेই পেতে চাইলে, আমাদের Ruby Magic নিউজলেটার সাবস্ক্রাইব করুন এবং কোনো পোস্ট মিস করবেন না!

  1. Unicorn কে Nginx-এ কানেক্ট করা: রুবিতে ইউনিক্স সকেটের প্র্যাক্টিক্যাল গাইড

  2. রুবিতে নিউরাল নেটওয়ার্ক ডিমিস্টিফাই: শুরুকারীদের গাইড

  3. রুবি ২.৬-এ ৯টি গুরুত্বপূর্ণ উন্নতির অন্বেষণ

  4. রুবি অবজেক্ট ডেকোরেটর প্যাটার্ন দিয়ে সমৃদ্ধ করুন: একটি প্র্যাক্টিক্যাল গাইড