الكتلة هي قطعة من الكود يمكن تمريرها إلى طريقة. وتُعرف هذه في لغات البرمجة الأخرى باسم دالة مجهولة أو لامدا. ويمكن للطريقة تحويل كتلة إلى Proc يمثّل كتلة من الكود.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
تُستخدم الكتل و Proc كثيرًا جدًا في Crystal، في أشياء مثل التكرار ودوال الاستدعاء.
ولكن أيضًا لإنشاء لغات محددة المجال (DSLs)، ومن أمثلة هذه اللغات إطار الويب Kemal أو إطار الاختبارات Spec (وهو ما يُستخدم لاختبار حلولك في Crystal).
لإنشاء طريقة تقبل كتلة، عليك إضافة علامة العطف (&) قبل الوسيط الأخير.
سيؤدي هذا إلى تحويل الكتلة إلى Proc وإسنادها إلى الوسيط.
ثم يؤدي استخدام yield إلى تنفيذ الكتلة المُمرَّرة إلى الطريقة.
def my_method(&block)
1 + yield
end
هناك طريقتان لتمرير كتلة: إما باستخدام الكلمتين المفتاحيتين do ... end، وإما باستخدام الأقواس المعقوفة ({}).
والفرق الرئيسي بينهما هو الأسبقية؛ فـ do ... end يدعم الكتل متعددة الأسطر، بينما لا تدعمها الأقواس المعقوفة ({}).
my_method do
2
end
# => 3
my_method { 2 }
# => 3
yieldyield كلمة مفتاحية تنفّذ الكتلة المُمرَّرة إلى الطريقة.
وهذا يعني أن الكتلة لن تُنفَّذ أبدًا إذا لم يُستدعَ yield.
ويمكن أيضًا أن تُعطى وسائط تُمرَّر إلى الكتلة.
ولا يوجد حد لعدد الوسائط التي يمكن تمريرها إلى الكتلة.
ولاستخدام الوسائط داخل الكتلة، تستعمل الصيغة |x, y, ...|.
حيث x و y و ... هي أسماء الوسائط.
def my_method(&block)
1 + yield 2
end
# With curly braces:
my_method { |x| x * 3 }
# => 7
# With do ... end:
my_method do |x|
x * 3
end
# => 7
في المثال أعلاه، تُعطى الكتلة الوسيط 2، فتضربه في 3 وتُرجع 6.
ثم تجمع الطريقة 1 إلى الناتج وتُرجع 7.
إذا كان عدد الوسائط المُعطاة إلى yield أكثر من عدد الوسائط التي تتوقعها الكتلة، فسيحدث خطأ.
def my_method(&block)
1 + yield
end
my_method { |x| x * 3 }
# Error: too many block parameters (given 1, expected maximum 0)
يمكن كتابة الكتل صراحةً أو ضمنًا. الكتل الضمنية هي الأكثر شيوعًا، وهي المستخدمة في الأمثلة أعلاه. بل يمكنك إعطاء الكتلة وسائط من أنواع مختلفة، وسيستنتج المترجم أنواعها وينشئ نوعًا اتحاديًا.
def my_method(&block)
yield "a"
yield 1
end
my_method do |x|
p typeof(x)
end
# output: (String | Int32)
عند كتابة كتل صريحة، يمكنك تحديد أنواع الوسائط ونوع الإرجاع.
ويتم ذلك عبر الصيغة Arg1Type, Arg2Type, ... -> ResultType.
حيث تفصل الفواصل (,) بين الوسائط، ويفصل -> نوع النتيجة.
def my_method(&block : Int32, Int32 -> Int32)
1 + yield(2, 3)
end
my_method { |x, y| x * y }
# => 7
وكما هو الحال مع الأنواع الصريحة الأخرى، سيفرض المترجم أن تكون الأنواع كما هو متوقع. وإذا لم تطابق الكتلة النوع المُعطى للطريقة، فسيحدث خطأ.
Procترتبط Proc ارتباطًا قويًا جدًا بالكتل.
والفرق الجوهري أن Proc نوع، أما الكتلة فليست كذلك.
يمثّل Proc مؤشر دالة ويمكن تمريره كمتغير.
لتعريف Proc، تستخدم صيغة proc الحرفية: ->(args) { ... }.
وتفصل الفواصل (,) بين الوسائط، وتحيط { ... } بالكتلة.
ولاستدعاء Proc، تستخدم الطريقة call وتمرّر إليها الوسائط.
أنواع المعاملات إلزامية.
ويُستنتج نوع الإرجاع من جسم الـ proc، ولكن يمكن أيضًا تحديده صراحةً.
my_proc = ->(x : Int32, y : Int32) : Int32 { x * y }
my_proc.call(2, 3)
# => 6
التقاط الكتل أسلوب لإنشاء Proc من كتلة.
ويتم ذلك باستخدام العامل & قبل الوسيط الأخير في الطريقة.
سيحوّل ذلك الكتلة إلى Proc ويسندها إلى الوسيط.
def my_method(&block)
block.call
end
my_method { 1 + 2 }
# => 3
Proc إلى الطرقيمكن تمرير Proc إلى الطرق التي تتوقع كتلة.
وقد يكون هذا مفيدًا عندما تريد تمرير كتل مختلفة بناءً على شرط معين.
def my_method(&block)
yield 5
end
my_proc = -> (x) { x * 2 }
my_method(&my_proc)
# => 10
عند التصريح بمتغير، يمكنك استخدام الصيغة : Type للتصريح بنوع المتغير.
أما بالنسبة إلى Proc، فتستخدم الصيغة Proc((ArgType, ...), ResultType).
وسيكون النوع الأخير دائمًا نوع الإرجاع، أما الباقي فستكون أنواع الوسائط.
أي أنه إذا كان هناك نوع واحد فقط، فسيكون هو نوع الإرجاع، ولن يأخذ Proc أي وسائط.
my_proc : Proc(Int32, Int32, Int32) = ->(x, y) { x * y }
other_proc : Proc(Int32) = -> { 1 }
أنت تطوّر نظام إدارة محتوى لموقع يستضيف المقالات والتدوينات وغيرها من المحتوى المكتوب. وقد أُوكلت إليك مهمة تنفيذ معالج مهام يعالج المهام في طابور ذي أولوية. يستقبل معالج المهام المهام، وبناءً على الحمل على الخادم وأولوية كل مهمة يقرر المهمة التالية التي سينفذها. إذا كان الخادم تحت حمل ثقيل، وجب أن يكون رقم الأولوية أعلى كي تُنفَّذ المهمة. سيغيّر الخادم الحمل ديناميكيًا بناءً على الحمل.
TaskHandler
يجب تهيئة TaskHandler بطابور وقيمة افتراضية لـ Array.
نفّذ الدالة المهيّئة للصنف String التي تأخذ مصفوفة من السلاسل النصية لأسماء المهام كوسيط وتخزّنها في متغير نسخة يُسمّى @tasks.
وعلى الدالة المهيّئة أيضًا أن تُنشئ متغير النسخة @task_condition_logic الذي يخزّن Proc يأخذ Int32 ويُرجع صحيحًا إذا كان الوسيط مساويًا للصفر أو أكبر منه، وخطأً إذا كان أقل من الصفر.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
# => #<TaskHandler:0x7fd8d4cfbf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562536a4a330>>
يجب أن يكون الخادم قادرًا على تحديث منطق شرط المهمة بناءً على حمل الخادم.
نفّذ الطريقة TaskHandler#update_task_condition_logic التي تأخذ كتلة كوسيط، ويجب تخزين هذه الكتلة في متغير النسخة @task_condition_logic.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
# => #<TaskHandler:0x7f068118cf00 @tasks=["task1", "task2", "task3"], @task_condition_logic=#<Proc(Int32, Bool):0x562df99d0370>>
يجب أن يكون الخادم قادرًا على تنفيذ المهمة التالية في الطابور.
أول مهمة في مصفوفة @tasks هي المهمة التالية التي ستُنفَّذ.
تُنفَّذ المهمة إذا كان مستوى أولويتها يُرجع صحيحًا عند تمريره إلى @task_condition_logic.
وتُرجع رسائل بناءً على ما إذا نُفِّذت المهمة أم لا.
إذا نُفِّذت المهمة فيجب أن تُرجع: "Completed <task>".
وإذا لم تُنفَّذ المهمة فيجب أن تُرجع: "Could not complete <task>".
وإذا نُفِّذت المهمة فيجب إزالتها من مصفوفة @tasks.
نفّذ الطريقة TaskHandler#execute_next_task التي تأخذ مستوى أولوية المهمة التالية كعدد Int32 في صورة وسيط، وتُرجع String بناءً على ما إذا نُفِّذت المهمة أم لا.
task_handler = TaskHandler.new(["task1", "task2", "task3"])
task_handler.update_task_condition_logic { |load| load > 1 }
task_handler.execute_next_task(2)
# => "Completed task1"
task_handler.execute_next_task(1)
# => "Could not complete task2"
سجّل في Exercism لتتعلّم وتتقن Crystal عبر 26 مفهومًا142 تمرينًا، وإرشاد بشري حقيقي، وكل ذلك مجانًا.