مطوّر Ruby يلقي نظرة على Crystal
نظرة أقرب على Crystal، لغة برمجة جديدة ومثيرة للاهتمام، يُقال إنها سريعة مثل C وأنيقة مثل Ruby.

لا نعاني اليوم من نقص في لغات البرمجة الجديدة والمثيرة للاهتمام. يكاد يكون من المستحيل أن تقضي بعض الوقت على Hacker News أو Twitter من دون أن ترى إعلانات عن لغات جديدة كل أسبوع. ولأنني مهووس كبير بلغات البرمجة، أقرأ عادة عن معظمها، لكن قلة منها فقط تنجح في الاحتفاظ باهتمامي طويلًا. غير أن Crystal واحدة من اللغات الجديدة التي نجحت في ذلك، وقد ساهم في ذلك بلا شك الشعار المكتوب على صفحتها في BountySource:
سريعة مثل C، وأنيقة مثل Ruby fancy-quote
أكتب بلغة Ruby منذ نحو عام 2004، وأحب اللغة وقدرتها التعبيرية. ومع أنها ليست حصان سباق بالتأكيد، فإنها سريعة بما يكفي لاحتياجاتي عمومًا. ومع ذلك، فإن احتمال وجود لغة مشابهة في صياغتها مع أداء ممتاز أمر مغرٍ، لذلك قررت أن أقضي بعض الوقت مع Crystal وأدوّن انطباعاتي.
الانطباعات الأولى
يذكر الموقع الإلكتروني للمشروع خمسة أهداف لـ Crystal، أولها "أن تكون صياغتها مشابهة لـ Ruby (لكن التوافق معها ليس هدفًا)".
تبدو Crystal بالفعل مثل Ruby. بل يمكن كتابة برامج (بسيطة) يقبلها كلا المُصرّفين (compilers). لكن كما ذُكر، التوافق ليس هدفًا، وربما يكون ذلك للأفضل، لأن دلالات اللغتين (semantics) مختلفة تمامًا في الواقع رغم التشابه في الصياغة.
يعطي تعريف الصنف (class) التالي انطباعًا أوليًا عن Crystal:
class Person
property age
getter name
def initialize(@name : String, @age : Int32)
end
end
p = Person.new("Michael", 35)
p.name #=> "Michael" : String
p.age #=> 35 : Int32
p.age += 1 #=> 36 : Int32
p.name = "Other person" # undefined method 'name=' for Person
lang-crystal
مع أن هذا يشبه Ruby كثيرًا، هناك بعض الفروق الملحوظة. تحل property وgetter، وهما أوضح في القراءة، محل attr_accessor وattr_reader. وتستخدم دالة initialize اختصارًا لإسناد معاملاتها مباشرة إلى متغيرات النسخة (instance variables)، كما تستخدم قيود الأنواع (type restrictions)، التي نادرًا ما نحتاج إليها بفضل قدرة Crystal الممتازة على استنتاج الأنواع (type inference).
يعرض المثال التالي فرقًا كبيرًا آخر بين اللغتين، وهو التحميل الزائد للدوال (method overloading) بحسب عدد المعاملات ونوعها، وهي ميزة طالما تمنيتها في Ruby:
class Dog
def greet
"Woof! Woof!"
end
def greet(name : String)
"Woof #{name}!"
end
def greet(times : Int32)
greet * times
end
end
d = Dog.new
d.greet #=> "Woof! Woof!" : String
d.greet("dear readers!") #=> "Woof dear readers!" : String
d.greet(3) #=> "Woof! Woof!Woof! Woof!Woof! Woof!" : String
lang-ruby
هنا نعرّف ثلاثة تطبيقات مختلفة لـ Dog#greet، ويوجّه المُصرّف الاستدعاء بشكل صحيح إلى النسخة التي تطابق عدد المعاملات ونوعها. شخصيًا أجد هذا أجمل بكثير من الشروط التي تتحقق من وجود المعاملات الاختيارية.
ومن المجالات الأخرى التي تتفوق فيها صياغة Crystal على Ruby الصيغة المختصرة للكتل (blocks) (انظروا Symbol#to_proc):
%w(foo bar).map(&.upcase) #=> ["FOO", "BAR"] : Array(String)
(1..5).map(&.+(2)) #=> [3, 4, 5, 6, 7] : Array(Int32)
lang-crystal
%w(foo bar).map(&:upcase) #=> ["FOO", "BAR"]
(1..5).map(&2.method(:+)) #=> [3, 4, 5, 6, 7]
lang-ruby
بينما تستخدم Ruby الصيغة &:method_name، تستخدم Crystal الصيغة &.method_name، وأجدها أوضح في التعبير عن القصد. كما تتيح تمرير معاملات إلى الدالة المستدعاة، وهو أمر غير ممكن عمومًا في Ruby (أو ممكن فقط ببعض الحيل كما في المثال أعلاه).
هناك فروق صياغية بسيطة أخرى، مثل أن النصوص (strings) تُحاط دائمًا بعلامات اقتباس مزدوجة (علامات الاقتباس المفردة تدل على قيم الأحرف الحرفية)، أو أن معدّلات الوصول (access modifiers) جزء من تعريف الدالة. لكن لا ينبغي أن يُربك أي منها مطوّري Ruby كثيرًا (وإن كنت أجد أحيانًا صعوبة في التغلب على ذاكرة العضلات).
الأنواع
Crystal لغة ذات أنواع ثابتة (statically typed). لكن هذا لا يعني أن كودكم يجب أن يمتلئ بتعليقات الأنواع (type annotations)، فالمُصرّف يؤدي عادة عملًا رائعًا في استنتاج الأنواع. ومع ذلك، هناك حالات معينة تحتاج فيها اللغة إلى مساعدتكم، وفي هذه الحالة ستعرض لكم رسالة خطأ مفيدة.
لنلقِ نظرة على مثال:
class Foo
def initialize(a)
@a = a
end
end
Foo.new(1)
lang-crystal
لن يُصرَّف هذا المثال الذي يبدو بريئًا، بل سيُنتج خطأ وقت التصريف التالي:
Can’t infer the type of instance variable ‘@a’ of Foo The type of a instance variable, if not declared explicitly with
@a : Type, is inferred from assignments to it across the whole program. The assignments must look like this: 1.@a = 1(or other literals), inferred to the literal’s type 2.@a = Type.new, type is inferred to be Type 3.@a = Type.method, wheremethodhas a return type annotation, type is inferred from it 4.@a = arg, with ‘arg’ being a method argument with a type restriction ‘Type’, type is inferred to be Type 5.@a = arg, with ‘arg’ being a method argument with a default value, type is inferred using rules 1, 2 and 3 from it 6.@a = uninitialized Type, type is inferred to be Type 7.@a = LibSome.func, andLibSomeis alib, type is inferred from that fun. 8.LibSome.func(out @a), andLibSomeis alib, type is inferred from that fun argument. Other assignments have no effect on its type. Can’t infer the type of instance variable ‘@a’ of Foo
مع أن هذه الرسالة طويلة نوعًا ما، فإنها تقدم شرحًا وافيًا لطريقة عمل استنتاج الأنواع في Crystal. والإصلاح الصحيح للبرنامج أعلاه هو إضافة قيد نوع على معامل الدالة، كما هو موضح في البند 4.
def initialize(a : Int32)
@a = a
end
# or shorter
def initialize(@a: Int32)
end
lang-crystal
أكاد أسمع مطوّري Ruby المخضرمين يسألون الآن: لماذا نهتم بالأنواع الثابتة أصلًا؟ والسؤال وجيه. لذا لنلقِ نظرة على مثال ينقذنا فيه استنتاج الأنواع في Crystal والاستخدام الذكي لـ أنواع الاتحاد (union types) من خطأ وقت التشغيل.
found = %w(foo bar).find { "foo" }
typeof(found) #=> (String | Nil)
found.upcase # undefined method 'upcase' for Nil (compile-time type is (String | Nil))
found.upcase if found #=> "FOO" : String
lang-crystal
هنا ستُرجع Enumerable#find إما نصًا وإما nil، وهو ما يؤدي في Ruby إلى RuntimeError حين لا يُعثر على أي عنصر ونحاول استدعاء دالة upcase على nil. أما مُصرّف Crystal فيستخدم هنا نوع الاتحاد String | Nil للمتغير found، ولن يُصرّف هذا الكود لأن ليس كل الأنواع في الاتحاد تعرف كيف تستجيب لرسالة upcase. لذا، لكي يُصرَّف هذا البرنامج فعلًا، علينا أن نتحقق صراحة من حالة nil كما هو موضح في السطر الأخير من الكود أعلاه.
كان هذا بالطبع مثالًا مصطنعًا وقصيرًا، لكنه يُظهر كيف أنقذتنا Crystal من خطأ أثناء تنفيذ البرنامج من دون أي عمل إضافي من جانبنا.
متفرقات
لنختم هذه النظرة الأولى على Crystal، دعونا نستعرض بسرعة بعض الميزات الجميلة الأخرى التي لا تقدمها Ruby.
Structs
تقدم Crystal أكثر من طريقة لتعريف الأصناف. فبدلًا من استخدام الكلمة المفتاحية class، يمكن تعريفها أيضًا باستخدام struct.
struct Point
property x, y
def initialize(@x : Int32, @y : Int32)
end
end
p = Point.new(5, 3)
p.class #=> Point : Class
lang-crystal
مع أن الكود أعلاه يعرّف صنفًا أيضًا، فإن كائنات Point ستُخصَّص في الـ stack وليس في الـ heap، وستُمرَّر بالقيمة (pass-by-value). شخصيًا أرى أنها إضافة رائعة للأنواع غير القابلة للتغيير (immutable)، تمنحكم تحكمًا أكبر في استهلاك برنامجكم للذاكرة.
Enums
تتيح لنا التعدادات (Enums) تجميع القيم المترابطة:
enum Suit
Spades
Diamonds
Clubs
Hearts
end
Suit::Spades.value #=> 0
lang-crystal
تُستخدم غالبًا في المواضع التي قد يستخدم فيها مطوّرو Ruby الرموز (symbols)، مع ميزة إضافية هي التحقق من الأنواع (أي أن كتابة Suit::Club بدلًا من Suit::Clubs ستؤدي إلى خطأ في المُصرّف، بينما قد تؤدي :club إلى سلوك غير صحيح وقت التشغيل). ولأن التعدادات يمكنها تعريف دوالها الخاصة تمامًا مثل الأصناف والـ structs، فهي مفيدة جدًا لتجميع القيم المترابطة والسلوك المرتبط بها.
Tuples
مثل Python، تمتلك Crystal نوع tuple. تُعرَّف الـ tuples بالصيغة {element1, element2,...}، وهي ممتازة لتجميع القيم المترابطة مؤقتًا. وتُستخدم داخليًا أيضًا في الإسناد المتعدد مثل a, b = 1, 2، حيث تستخدم Ruby بدلًا منها Array.
الخلاصة
Crystal لغة جميلة تشبه Ruby كثيرًا في إحساسها، مع أنها تُصرَّف إلى كود سريع وفعّال عبر LLVM. أرجو أن تكون هذه المقدمة القصيرة قد نجحت في إثارة اهتمامكم باللغة، وربما حفزتكم على تجربتها. في المقال القادم أنوي استعراض بعض الميزات الأكثر تقدمًا، مثل البرمجة الوصفية (metaprogramming) باستخدام الماكرو، والتكامل مع C، والتزامن (concurrency). ترقبوا!


