مقدمة: لماذا مقارنة Rust وHaskell؟
\n\nإذا سبق لك أن ألقيت نظرة على Stack Overflow بحثًا عن إجابة لسؤال "What is the difference between traits in Rust and typeclasses in Haskell?"، فأنت تعلم أن الموضوع ساخن. آلاف المشاهدات وعشرات الأصوات - مجتمع المطورين يريد حقًا فهم كيف يختلف هاتان الآليتان.
\n\nللوهلة الأولى، تبدو متشابهة كالأخوة: فكل من السمات (traits) في Rust وفئات الأنواع (typeclasses) في Haskell تسمح بتعريف سلوك مشترك لأنواع مختلفة. ولكن إذا تعمقت أكثر، ستكتشف أن الفلسفة والتنفيذ والتطبيق العملي لكل منهما - عوالم مختلفة.
\n\nفي هذه المقالة، سنحلل الاختلافات الرئيسية من خلال أمثلة حقيقية لكود بلغة Rust (مع بعض الأمثلة بلغة Haskell للمقارنة)، وفي النهاية ستحصل على فهم واضح: متى تستخدم السمات (traits)، وأين من الأفضل التفكير في نهج مشابه لفئات الأنواع (typeclass).
\n\nنظرة عامة سريعة: ما هي السمات (Traits) في Rust؟
\n\nالسمات (Traits) في Rust هي طريقة للإعلان عن مجموعة من الدوال التي يجب تنفيذها لنوع معين. هذا هو أساس البرمجة العامة (generics) وتعدد الأشكال في Rust.
\n\nمثال بسيط: تخيل أننا نريد أن تتمكن أنواع مختلفة من "إلقاء التحية".
\n\ntrait Greeter {\n fn greet(&self) -> String;\n}\n\nstruct Person {\n name: String,\n}\n\nimpl Greeter for Person {\n fn greet(&self) -> String {\n format!("Привет, меня зовут {}!", self.name)\n }\n}\n\nstruct Robot {\n model: String,\n}\n\nimpl Greeter for Robot {\n fn greet(&self) -> String {\n format!("Бип-буп, модель: {}", self.model)\n }\n}\n\nfn say_hello(item: &impl Greeter) {\n println!("{}", item.greet());\n}\n\nfn main() {\n let person = Person { name: "Анна".into() };\n let robot = Robot { model: "R2-D2".into() };\n say_hello(&person);\n say_hello(&robot);\n}\n\nهنا Greeter هو سمة (trait). النوعان Person و Robot يقومان بتنفيذها بطريقتهما الخاصة. الدالة say_hello تقبل أي نوع ينفذ Greeter. هذا هو تعدد الأشكال عبر السمات (traits).
نظرة عامة سريعة: ما هي فئات الأنواع (Typeclasses) في Haskell؟
\n\nفئة النوع (Typeclass) في Haskell هي واجهة تحدد مجموعة من الدوال التي يمكن تنفيذها لأنواع مختلفة. يبدو مألوفًا، أليس كذلك؟
\n\nهذا هو الشكل الذي سيبدو عليه المثال المماثل بلغة Haskell:
\n\nclass Greeter a where\n greet :: a -> String\n\ndata Person = Person { name :: String }\ndata Robot = Robot { model :: String }\n\ninstance Greeter Person where\n greet (Person n) = "Привет, меня зовут " ++ n ++ "!"\n\ninstance Greeter Robot where\n greet (Robot m) = "Бип-буп, модель: " ++ m\n\nsayHello :: Greeter a => a -> IO ()\nsayHello item = putStrLn (greet item)\n\nmain = do\n sayHello (Person "Анна")\n sayHello (Robot "R2-D2")\n\nبناء الجملة مختلف، لكن المنطق هو نفسه. ومع ذلك، فإن الاختلافات بين السمات (traits) في Rust وفئات الأنواع (typeclasses) في Haskell تبدأ من هنا.
\n\nالاختلاف الرئيسي رقم 1: التحويل الأحادي الشكل (Monomorphization) مقابل التغليف (Box)
\n\nالاختلاف الأكثر جوهرية بين السمات (traits) وفئات الأنواع (typeclasses) يكمن في كيفية معالجة المترجم للكود العام.
\n\nRust: التحويل الأحادي الشكل (Monomorphization)
\n\nعندما تكتب fn say_hello(item: &impl Greeter)، يقوم مترجم Rust بإنشاء نسخة منفصلة من الدالة لكل نوع محدد. وهذا ما يسمى التحويل الأحادي الشكل (monomorphization). النتيجة: تجريد بتكلفة صفرية (zero-cost abstraction) - أنت لا تدفع ثمن ما لا تستخدمه. لكن حجم الكود قد يزداد.
Haskell: التغليف (Boxing) (افتراضيًا)
\n\nفي Haskell، يتم تنفيذ فئات الأنواع (typeclasses) عادةً من خلال قواميس الدوال (vtable). يمرر المترجم معاملًا ضمنيًا - جدولًا بمؤشرات للدوال. وهذا ما يسمى التغليف (boxing). أنت تدفع ثمن