কম্পিউটার

ActiveSupports #descendants পদ্ধতি:একটি গভীর ডুব

রেল রুবির অন্তর্নির্মিত বস্তুতে অনেক কিছু যোগ করে। এটিকে কেউ কেউ রুবির "উপভাষা" বলে এবং এটিই রেল বিকাশকারীদের 1.day.ago এর মতো লাইন লিখতে দেয় .

এই অতিরিক্ত পদ্ধতিগুলির বেশিরভাগই ActiveSupport-এ থাকে। আজ, আমরা সম্ভবত একটি স্বল্প পরিচিত পদ্ধতি দেখতে যাচ্ছি যা ActiveSupport সরাসরি ক্লাসে যোগ করে:descendants . এই পদ্ধতিটি বলা ক্লাসের সমস্ত সাবক্লাস রিটার্ন করে। উদাহরণস্বরূপ, ApplicationRecord.descendants আপনার অ্যাপের সেই ক্লাসগুলি ফিরিয়ে দেবে যা এটি থেকে উত্তরাধিকারসূত্রে পাওয়া যায় (যেমন, আপনার অ্যাপ্লিকেশনের সমস্ত মডেল)। এই নিবন্ধে, আমরা এটি কীভাবে কাজ করে, কেন আপনি এটি ব্যবহার করতে চান এবং কীভাবে এটি রুবির অন্তর্নির্মিত উত্তরাধিকার-সম্পর্কিত পদ্ধতিগুলিকে বাড়িয়ে তোলে তা দেখব৷

অবজেক্ট-ওরিয়েন্টেড ভাষায় উত্তরাধিকার

প্রথমে, আমরা রুবির উত্তরাধিকার মডেলে একটি দ্রুত রিফ্রেশার প্রদান করব। অন্যান্য অবজেক্ট-ওরিয়েন্টেড (OO) ভাষার মতো, রুবি এমন বস্তু ব্যবহার করে যা একটি শ্রেণিবিন্যাসের মধ্যে বসে। আপনি একটি ক্লাস তৈরি করতে পারেন, তারপর সেই ক্লাসের একটি সাবক্লাস, তারপর সেই সাবক্লাসের একটি সাবক্লাস এবং আরও অনেক কিছু। এই শ্রেণিবিন্যাসের দিকে হাঁটার সময়, আমরা পূর্বপুরুষদের একটি তালিকা পাই। রুবিরও চমৎকার বৈশিষ্ট্য রয়েছে যা সমস্ত সত্তাগুলি নিজেই বস্তু (শ্রেণী, পূর্ণসংখ্যা এবং এমনকি শূন্য সহ), যেখানে কিছু অন্যান্য ভাষা প্রায়ই "আদিম" ব্যবহার করে যেগুলি সত্য বস্তু নয়, সাধারণত কার্য সম্পাদনের জন্য (যেমন পূর্ণসংখ্যা, দ্বিগুণ, বুলিয়ান ইত্যাদি; আমি' আমি তোমাকে দেখছি, জাভা)।

রুবি এবং প্রকৃতপক্ষে, সমস্ত OO ভাষাগুলিকে পূর্বপুরুষদের ট্র্যাক রাখতে হবে যাতে এটি জানতে পারে কোথায় পদ্ধতিগুলি সন্ধান করতে হবে এবং কোনটি অগ্রাধিকার পাবে৷

class BaseClass
  def base
    "base"
  end

  def overridden
    "Base"
  end
end

class SubClass < BaseClass
  def overridden
    "Subclass"
  end
end

এখানে, SubClass.new.overridden কল করা হচ্ছে আমাদের "SubClass" দেয় . যাইহোক, SubClass.new.base আমাদের সাবক্লাস সংজ্ঞায় উপস্থিত নয়, তাই রুবি প্রতিটি পূর্বপুরুষের মধ্য দিয়ে যাবেন তা দেখতে কোনটি পদ্ধতিটি প্রয়োগ করে (যদি থাকে)। আমরা কেবল SubClass.ancestors কল করে পূর্বপুরুষদের তালিকা দেখতে পারি . রেলে, ফলাফলটি এরকম কিছু হবে:

[SubClass,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 Object,
 PP::ObjectMixin,
 JSON::Ext::Generator::GeneratorMethods::Object,
 ActiveSupport::Tryable,
 ActiveSupport::Dependencies::Loadable,
 Kernel,
 BasicObject]

আমরা এখানে এই পুরো তালিকাটি ব্যবচ্ছেদ করব না; আমাদের উদ্দেশ্যে, এটা মনে রাখা যথেষ্ট যে SubClass BaseClass সহ শীর্ষে রয়েছে এর নিচে এছাড়াও, মনে রাখবেন যে BasicObject নীচে আছে; এটি রুবিতে শীর্ষ-স্তরের অবজেক্ট, তাই এটি সর্বদা স্ট্যাকের নীচে থাকবে৷

মডিউল (a.k.a. 'Mixins')

যখন আমরা মিশ্রণে মডিউল যোগ করি তখন জিনিসগুলি আরও জটিল হয়ে যায়। একটি মডিউল শ্রেণী অনুক্রমের পূর্বপুরুষ নয়, তবুও আমরা এটিকে আমাদের ক্লাসে "অন্তর্ভুক্ত" করতে পারি তাই রুবিকে জানতে হবে কখন একটি পদ্ধতির জন্য মডিউলটি পরীক্ষা করতে হবে, বা একাধিক মডিউল অন্তর্ভুক্ত হওয়ার ক্ষেত্রে কোন মডিউলটি প্রথমে পরীক্ষা করতে হবে। .

কিছু ভাষা এই ধরনের "একাধিক উত্তরাধিকার" এর অনুমতি দেয় না, তবে রুবি আরও এক ধাপ এগিয়ে যায় যে আমরা মডিউলটি অন্তর্ভুক্ত বা প্রিপেন্ড করে মডিউলটি অনুক্রমের মধ্যে কোথায় ঢোকানো হবে তা বেছে নিতে দেয়।

প্রিপেন্ডিং মডিউল

প্রিপেন্ডেড মডিউলগুলি, যেমন তাদের নাম কিছুটা প্রস্তাব করে, ক্লাসের আগে পূর্বপুরুষদের তালিকায় সন্নিবেশ করা হয়, মূলত ক্লাসের যে কোনো পদ্ধতিকে অগ্রাহ্য করে। এর মানে হল আপনি একটি প্রিপেন্ডেড মডিউলের পদ্ধতিতে "সুপার" কে মূল ক্লাসের পদ্ধতিতে কল করতে পারেন৷

module PrependedModule
  def test
    "module"
  end

  def super_test
    super
  end
end

# Re-using `BaseClass` from earlier
class SubClass < BaseClass
  prepend PrependedModule

  def test
    "Subclass"
  end

  def super_test
    "Super calls SubClass"
  end
end

সাবক্লাসের পূর্বপুরুষরা এখন এইরকম দেখাচ্ছে:

[PrependedModule,
 SubClass,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ...
]

পূর্বপুরুষদের এই নতুন তালিকার সাথে, আমাদের PrependedModule এখন ফার্স্ট-ইন-লাইন, মানে আমরা SubClass-এ কল করি এমন যেকোনো পদ্ধতির জন্য রুবি প্রথমে সেখানে দেখবে। . এটি এছাড়াও৷ মানে যদি আমরা super কল করি PrependedModule-এর মধ্যে , আমরা SubClass-এ পদ্ধতিটিকে কল করব :

> SubClass.new.test
=> "module"
> SubClass.new.super_test
=> "Super calls SubClass"

মডিউল সহ

অপরদিকে, অন্তর্ভুক্ত মডিউলগুলি পূর্বপুরুষের মধ্যে ঢোকানো হয় পরে শ্রেণী. এটি তাদের বাধা দেওয়ার পদ্ধতির জন্য আদর্শ করে তোলে যা অন্যথায় বেস ক্লাস দ্বারা পরিচালিত হবে।

class BaseClass
  def super_test
    "Super calls base class"
  end
end

module IncludedModule
  def test
    "module"
  end

  def super_test
    super
  end
end

class SubClass < BaseClass
  include IncludedModule

  def test
    "Subclass"
  end
end

এই ব্যবস্থার সাথে, সাবক্লাসের পূর্বপুরুষগুলি এখন এইরকম দেখাচ্ছে:

[SubClass,
 IncludedModule,
 BaseClass,
 ActiveSupport::Dependencies::ZeitwerkIntegration::RequireDependency,
 ...
]

এখন, সাবক্লাস হল কলের প্রথম পয়েন্ট, তাই রুবি শুধুমাত্র IncludedModule-এ পদ্ধতিগুলি চালাবে যদি তারা SubClass-এ উপস্থিত না থাকে . যেমন super , super-এ যেকোনো কল SubClass-এ IncludedModule-এ যাবে প্রথমে, super এ কল করার সময় এর মধ্যে IncludedModule BaseClass-এ যাবে .

অন্যভাবে বলুন, একটি অন্তর্ভূক্ত মডিউল একটি সাবক্লাস এবং তার বেস ক্লাসের মধ্যে পূর্বপুরুষ শ্রেণিবিন্যাসে বসে। এর কার্যকরী অর্থ হল এগুলিকে 'ইন্টারসেপ্ট' পদ্ধতিতে ব্যবহার করা যেতে পারে যা অন্যথায় বেস ক্লাস দ্বারা পরিচালনা করা হবে:

> SubClass.new.test
=> "Subclass"
> SubClass.new.super_test
=> "Super calls BaseClass"

এই "চেইন অফ কমান্ড" এর কারণে, রুবিকে এক শ্রেণীর পূর্বপুরুষদের ট্র্যাক রাখতে হয়। যদিও বিপরীতটি সত্য নয়। একটি নির্দিষ্ট শ্রেণী প্রদত্ত, রুবিকে তার সন্তানদের, বা "বংশদের" ট্র্যাক করার দরকার নেই, কারণ কোনও পদ্ধতি চালানোর জন্য এটির কখনই এই তথ্যের প্রয়োজন হবে না৷

পূর্বপুরুষের আদেশ

বিচক্ষণ পাঠকরা হয়তো বুঝতে পেরেছেন যে আমরা যদি একটি ক্লাসে একাধিক মডিউল ব্যবহার করি, তাহলে আমরা যে ক্রমটি অন্তর্ভুক্ত করি (বা আগে থেকে) তা ভিন্ন ফলাফল আনতে পারে। উদাহরণস্বরূপ, পদ্ধতির উপর নির্ভর করে, এই ক্লাস:

class SubClass < BaseClass
  include IncludedModule
  include IncludedOtherModule
end

এবং এই ক্লাস:

class SubClass < BaseClass
  include IncludedOtherModule
  include IncludedModule
end

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

বাস্তব-বিশ্ব ব্যবহার

যদিও include-এর মধ্যে পার্থক্য জেনে রাখা ভালো এবং prepend মডিউলগুলির জন্য, আমি মনে করি একটি বাস্তব-বিশ্বের উদাহরণ দেখাতে সাহায্য করে যখন আপনি অন্যটির উপর একটি বেছে নিতে পারেন। এই ধরনের মডিউলগুলির জন্য আমার প্রধান ব্যবহার-কেস হল রেল ইঞ্জিনগুলির সাথে৷

সম্ভবত সবচেয়ে জনপ্রিয় রেল ইঞ্জিনগুলির মধ্যে একটি হল উদ্ভাবন। ধরা যাক আমরা ব্যবহার করা পাসওয়ার্ড ডাইজেস্ট অ্যালগরিদম পরিবর্তন করতে চেয়েছিলাম, কিন্তু প্রথমে, একটি দ্রুত দাবিত্যাগ:

আমার প্রতিদিনের মডিউলগুলির ব্যবহার হল একটি রেল ইঞ্জিনের আচরণ কাস্টমাইজ করা যা আমাদের ডিফল্ট ব্যবসায়িক যুক্তি ধারণ করে। আমরা আমরা নিয়ন্ত্রণ করি কোডের আচরণ ওভাররাইড করছি . আপনি, অবশ্যই, রুবির যেকোনো অংশে একই পদ্ধতি প্রয়োগ করতে পারেন, কিন্তু আমি ওভাররাইডিং কোডের সুপারিশ করব না যা আপনি নিয়ন্ত্রণ করেন না (যেমন, অন্য লোকেদের দ্বারা রক্ষণাবেক্ষণ করা রত্ন থেকে), কারণ সেই বাহ্যিক কোডে যে কোনও পরিবর্তন আপনার পরিবর্তনের সাথে বেমানান হতে পারে৷

Devise এর পাসওয়ার্ড ডাইজেস্ট এখানে ঘটে Devise::Models::DatabaseAuthenticatable মডিউল:

  def password_digest(password)
    Devise::Encryptor.digest(self.class, password)
  end

  # and also in the password check:
  def valid_password?(password)
    Devise::Encryptor.compare(self.class, encrypted_password, password)
  end

ডিভাইস আপনাকে আপনার নিজস্ব Devise::Encryptable::Encryptors তৈরি করে এখানে ব্যবহৃত অ্যালগরিদম কাস্টমাইজ করতে দেয়। , যা এটি করার সঠিক উপায়। প্রদর্শনের উদ্দেশ্যে, তবে, আমরা একটি মডিউল ব্যবহার করব।

# app/models/password_digest_module
module PasswordDigestModule
  def password_digest(password)
    # Devise's default bcrypt is better for passwords,
    # using sha1 here just for demonstration
    Digest::SHA1.hexdigest(password)
  end

  def valid_password?(password)
    Devise.secure_compare(password_digest(password), self.encrypted_password)
  end
end

begin
  User.include(PasswordDigestModule)
# Pro-tip - because we are calling User here, ActiveRecord will
# try to read from the database when this class is loaded.
# This can cause commands like `rails db:create` to fail.
rescue ActiveRecord::NoDatabaseError, ActiveRecord::StatementInvalid
end

এই মডিউলটি লোড করার জন্য, আপনাকে কল করতে হবে Rails.application.eager_load! ফাইলটি লোড করতে ডেভেলপমেন্টে বা একটি রেল ইনিশিয়ালাইজার যোগ করুন। এটি পরীক্ষা করে, আমরা দেখতে পারি এটি প্রত্যাশিত হিসাবে কাজ করে:

> User.create!(email: "one@test.com", name: "Test", password: "TestPassword")
=> #<User id: 1, name: "Test", created_at: "2021-05-01 02:08:29", updated_at: "2021-05-01 02:08:29", posts_count: nil, email: "one@test.com">
> User.first.valid_password?("TestPassword")
=> true
> User.first.encrypted_password
=> "4203189099774a965101b90b74f1d842fc80bf91"

আমাদের ক্ষেত্রে এখানে, উভয়ই include এবং prepend একই ফলাফল হবে, কিন্তু এর একটি জটিলতা যোগ করা যাক. কি হবে যদি আমাদের ব্যবহারকারী মডেল তার নিজস্ব password_salt প্রয়োগ করে পদ্ধতি, কিন্তু আমরা এটিকে আমাদের মডিউল পদ্ধতিতে ওভাররাইড করতে চাই:

class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  has_many :posts

  def password_salt
    # Terrible way to create a password salt,
    # purely for demonstration purposes
    Base64.encode64(email)[0..-4]
  end
end

তারপরে, আমরা আমাদের মডিউলটি তার নিজস্ব password_salt ব্যবহার করতে আপডেট করি পাসওয়ার্ড ডাইজেস্ট তৈরি করার সময় পদ্ধতি:

  def password_digest(password)
    # Devise's default bcrypt is better for passwords,
    # using sha1 here just for demonstration
    Digest::SHA1.hexdigest(password + "." + password_salt)
  end

  def password_salt
    # an even worse way of generating a password salt
    "salt"
  end

এখন, include এবং prepend ভিন্নভাবে আচরণ করবে কারণ আমরা কোনটি ব্যবহার করি তা নির্ধারণ করবে কোনটি password_salt রুবি চালানোর পদ্ধতি। prepend দিয়ে , মডিউল অগ্রাধিকার নেবে, এবং আমরা এটি পাই:

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.salt"

include ব্যবহার করতে মডিউল পরিবর্তন করা হচ্ছে পরিবর্তে এর অর্থ হবে যে ব্যবহারকারী শ্রেণি বাস্তবায়ন অগ্রাধিকার নেয়:

> User.last.password_digest("test")
=> "a94a8fe5ccb19ba61c4c0873d391e987982fbbd3.dHdvQHRlc3QuY2"

সাধারণত, আমি prepend-এর জন্য পৌঁছাই প্রথম কারণ, একটি মডিউল লেখার সময়, আমি এটিকে আরও একটি সাবক্লাসের মতো আচরণ করা সহজ মনে করি এবং ধরে নিই যে মডিউলের যেকোনো পদ্ধতি ক্লাসের সংস্করণটিকে ওভাররাইড করবে। স্পষ্টতই, এটি সর্বদা কাঙ্খিত হয় না, এই কারণেই রুবি আমাদের include দেয় বিকল্প।

বংশধর

আমরা দেখেছি যে রুবি কীভাবে পদ্ধতিগুলি চালানোর সময় অগ্রাধিকারের ক্রম জানতে ক্লাস পূর্বপুরুষদের ট্র্যাক রাখে, সেইসাথে আমরা কীভাবে মডিউলগুলির মাধ্যমে এই তালিকায় এন্ট্রি সন্নিবেশ করি। যাইহোক, প্রোগ্রামার হিসাবে, এটি একটি ক্লাসের সকলের মাধ্যমে পুনরাবৃত্তি করা দরকারী হতে পারে , খুব এখানেই ActiveSupport-এর #descendants পদ্ধতিটি আসে। পদ্ধতিটি বেশ সংক্ষিপ্ত এবং প্রয়োজনে রেলের বাইরে সহজেই অনুলিপি করা হয়:

class Class
  def descendants
    ObjectSpace.each_object(singleton_class).reject do |k|
      k.singleton_class? || k == self
    end
  end
end

অবজেক্টস্পেস রুবির একটি খুব আকর্ষণীয় অংশ যা বর্তমানে মেমরিতে থাকা প্রতিটি রুবি অবজেক্ট সম্পর্কে তথ্য সংরক্ষণ করে। আমরা এখানে এটিতে ডুব দেব না, তবে যদি আপনার অ্যাপ্লিকেশনে একটি ক্লাস সংজ্ঞায়িত থাকে (এবং এটি লোড করা হয়েছে), তবে এটি অবজেক্টস্পেসে উপস্থিত থাকবে। ObjectSpace#each_object , যখন একটি মডিউল পাস করা হয়, শুধুমাত্র সেই বস্তুগুলি প্রদান করে যা মডিউলের সাবক্লাসগুলির সাথে মেলে; এখানে ব্লকটি শীর্ষ স্তরকেও প্রত্যাখ্যান করে (যেমন, যদি আমরা Numeric.descendants কল করি , আমরা Numeric আশা করি না ফলাফলে থাকতে হবে)।

এখানে যা ঘটছে তা যদি আপনি পুরোপুরি না পান তবে চিন্তা করবেন না, কারণ এটি পেতে অবজেক্টস্পেসে আরও পড়ার সম্ভবত প্রয়োজন। আমাদের উদ্দেশ্যে, এটা জানা যথেষ্ট যে এই পদ্ধতিটি Class-এ থাকে এবং বংশধর শ্রেণীর একটি তালিকা প্রদান করে, অথবা আপনি এটিকে সেই শ্রেণীর সন্তান, নাতি-নাতনি ইত্যাদির "পারিবারিক গাছ" হিসাবে ভাবতে পারেন৷

#descendants-এর বাস্তব-বিশ্ব ব্যবহার

2018 RailsConf-এ, রায়ান লাফলিন 'চেকআপ' ​​বিষয়ে একটি বক্তৃতা দিয়েছেন। ভিডিওটি দেখার মতো, কিন্তু আমরা শুধু একটি ধারণা বের করব, যা পর্যায়ক্রমে আপনার ডাটাবেসের সমস্ত সারির মাধ্যমে চালানো এবং তারা আপনার মডেলের বৈধতা পরীক্ষায় উত্তীর্ণ হয়েছে কিনা তা পরীক্ষা করা। আপনি অবাক হতে পারেন আপনার ডাটাবেসের কতগুলি সারি #valid? পাস করে না পরীক্ষা।

প্রশ্ন, তাহলে, ম্যানুয়ালি মডেলগুলির একটি তালিকা বজায় না রেখে আমরা কীভাবে এই চেকটি বাস্তবায়ন করব? #descendants উত্তর হল:

# Ensure all models are loaded (should not be necessary in production)
Rails.application.load! if Rails.env.development?

ApplicationRecord.descendants.each do |model_class|
  # in the real world you'd want to send this off to background job(s)
  model_class.all.each do |record|
    if !record.valid?
      HoneyBadger.notify("Invalid #{model.name} found with ID: #{record.id}")
    end
  end
end

এখানে, ApplicationRecord.descendants একটি আদর্শ রেল অ্যাপ্লিকেশনে আমাদের প্রতিটি মডেলের একটি তালিকা দেয়। আমাদের লুপে, তারপর, model ক্লাস (যেমন, User অথবা Product ) এখানে বাস্তবায়নটি বেশ মৌলিক, কিন্তু ফলাফল হল এটি প্রতিটি মডেলের মাধ্যমে পুনরাবৃত্তি করবে (বা, আরও সঠিকভাবে, ApplicationRecord-এর প্রতিটি সাবক্লাস) এবং .valid? কল করবে। প্রতিটি সারির জন্য।

উপসংহার

বেশিরভাগ রেল ডেভেলপারদের জন্য, মডিউল সাধারণত ব্যবহার করা হয় না। এটি একটি সঙ্গত কারণে; আপনি যদি কোডের মালিক হন, তাহলে সাধারণত এর আচরণ কাস্টমাইজ করার সহজ উপায় আছে এবং যদি আপনি না করেন কোডের মালিক, মডিউলগুলির সাথে এর আচরণ পরিবর্তন করার ঝুঁকি রয়েছে। তবুও, তাদের ব্যবহার-ক্ষেত্র রয়েছে, এবং এটি রুবির নমনীয়তার একটি প্রমাণ যে আমরা কেবল অন্য ফাইল থেকে একটি ক্লাস পরিবর্তন করতে পারি না, আমাদের কাছে কোথায় বেছে নেওয়ার বিকল্পও রয়েছে। পূর্বপুরুষ শৃঙ্খলে আমাদের মডিউল উপস্থিত হয়।

ActiveSupport তারপর #ancestors এর বিপরীত প্রদান করতে আসে #descendants সহ . আমি যতদূর দেখেছি এই পদ্ধতিটি খুব কমই ব্যবহৃত হয়, তবে একবার আপনি এটি সেখানে আছে তা জানলে, আপনি সম্ভবত এটির জন্য আরও বেশি ব্যবহার খুঁজে পাবেন। ব্যক্তিগতভাবে, আমি এটি শুধুমাত্র মডেলের বৈধতা পরীক্ষা করার জন্য ব্যবহার করেছি না, এমনকি আমরা সঠিকভাবে attribute_alias যোগ করছি তা যাচাই করার জন্য চশমা সহ ব্যবহার করেছি। আমাদের সকল মডেলের জন্য পদ্ধতি।


  1. রুবি ফাংশন এবং পদ্ধতি:আপনার নিজের সংজ্ঞায়িত কিভাবে

  2. রুবি নির্বাচন পদ্ধতি কিভাবে ব্যবহার করবেন (উদাহরণ সহ)

  3. রুবি আলিয়াস কীওয়ার্ড কীভাবে ব্যবহার করবেন

  4. রুবি ফ্রিজ পদ্ধতি - বস্তুর পরিবর্তনশীলতা বোঝা