یک بلوک تکهکدی است که میتوان آن را به یک متد پاس داد.
این مفهوم در زبانهای برنامهنویسی دیگر با نام تابع بینام یا لامبدا شناخته میشود.
یک متد میتواند یک بلوک را به یک Proc تبدیل کند که نمایندهی یک بلوک کد است.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
بلوکها و Procها در Crystal بسیار رایجاند، برای کارهایی مثل تکرار و کالبک.
اما همچنین برای ساختن زبانهای دامنهویژه (DSL). چند نمونه از آنها فریمورک وب 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
yield کلیدواژهای است که بلوک دادهشده به متد را اجرا میکند.
یعنی اگر 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 یعنی ->(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ها را به متدهایی پاس داد که انتظار یک بلوک را دارند. این کار وقتی به کار میآید که بخواهید بر اساس شرطی خاص، بلوکهای متفاوتی پاس دهید.
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 باید با یک صف و یک load_level پیشفرض مقداردهی اولیه شود.
سازندهی کلاس TaskHandler را پیادهسازی کنید. این سازنده یک Array از Stringها (اسم کارها) را به عنوان آرگومان میگیرد و آن را در متغیر نمونهای به اسم @tasks ذخیره میکند.
سازنده همچنین باید متغیر نمونهای @task_condition_logic را ایجاد کند که یک Proc را ذخیره میکند؛ این 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"