مسیرها
/
Crystal
Crystal
/
تمرین‌ها
/
گرداننده‌ی کارها
گرداننده‌ی کارها

گرداننده‌ی کارها

تمرین یادگیری

مقدمه

یک بلوک تکه‌کدی است که می‌توان آن را به یک متد پاس داد. این مفهوم در زبان‌های برنامه‌نویسی دیگر با نام تابع بی‌نام یا لامبدا شناخته می‌شود. یک متد می‌تواند یک بلوک را به یک 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 کلیدواژه‌ای است که بلوک داده‌شده به متد را اجرا می‌کند. یعنی اگر 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 }

دستورالعمل‌ها

شما در حال توسعه‌ی یک سیستم مدیریت محتوا برای وب‌سایتی هستید که مقاله‌ها، پست‌های وبلاگ و سایر محتوای نوشتاری را میزبانی می‌کند. به شما سپرده شده است که یک مدیریت‌کننده‌ی کارها پیاده‌سازی کنید که کارها را در یک صف اولویت‌دار پردازش می‌کند. مدیریت‌کننده‌ی کارها کارها را دریافت می‌کند و بر اساس بار سرور و اولویتشان تصمیم می‌گیرد که کدام کار را در ادامه اجرا کند. اگر سرور زیر بار سنگینی باشد، برای اینکه کار اجرا شود، عدد اولویت باید بالاتر باشد. سرور بار را به‌صورت پویا بر اساس بار تغییر می‌دهد.

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>>

2. منطق شرط کار را به‌روزرسانی کنید

سرور باید بتواند منطق شرط کار را بر اساس بار سرور به‌روزرسانی کند.

متد 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>>

3. کار بعدی را اجرا کنید

سرور باید بتواند کار بعدی را در صف اجرا کند.

اولین کار در آرایه‌ی @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"
ویرایش از طریق GitHub این لینک در پنجره یا زبانه‌ی جدیدی باز می‌شود
Crystal Exercism

آماده‌اید گرداننده‌ی کارها را شروع کنید؟

در Exercism ثبت‌نام کنید تا Crystal را همراه با 26 مفهوم142 تمرین و مربی‌گری انسانی واقعی یاد بگیرید و در آن استاد شوید، همه‌ی این‌ها رایگان.