«بلوک» قطعهای از کد است که میتوان آن را به یک متد پاس داد. در زبانهای برنامهنویسی دیگر به این مفهوم تابع بینام یا لامبدا میگویند. یک متد میتواند یک بلوک را به Proc تبدیل کند، که نمایندهی یک بلوک کد است.
def my_method(&block)
1 + yield
end
my_method { 2 }
# => 3
بلوکها و Procها در Crystal بسیار پرکاربردند، مثلاً برای کارهایی مانند پیمایش و «callback»ها. اما همچنین برای ساختن زبانهای خاص دامنه (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)
بلوکها را میتوان بهصورت صریح یا ضمنی نوشت. بلوکهای ضمنی رایجتریناند و همانهایی هستند که در مثالهای بالا دیدید. حتی میتوانید به بلوک آرگومانهایی با انواع مختلف بدهید؛ کامپایلر انواع را استنتاج میکند و یک نوع union میسازد.
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
مانند سایر انواع صریح، کامپایلر هم بررسی میکند که انواع همان چیزی باشند که انتظار میرود. اگر بلوک با نوعی که به متد داده شده سازگار نباشد، خطا رخ میدهد.
شکل مختصری برای بلوکهایی وجود دارد که فقط یک آرگومان میگیرند و فقط یک متد را روی آن فراخوانی میکنند.
بهجای استفاده از |x| میتوانید پیش از اسم آرگومان از & استفاده کنید.
def my_method(&block)
yield "a"
end
# This is:
my_method { |x| x.upcase }
# => "A"
# Is the same as:
my_method &.upcase
# => "A"
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 }
همچنین میتوانید یک proc را از روی یک متد اعلام کنید.
def my_method(a, b)
a + b
end
my_proc = ->my_method(Int32, Int32)
my_proc.call(2, 3)