Блоки и Proc
Методы могут принимать блок кода, который выполняется с помощью ключевого слова yield. Например:
def twice
yield
yield
end
twice do
puts "Hello!"
end
Приведённая выше программа дважды выводит «Hello!» — по одному разу для каждого yield.
Чтобы определить метод, принимающий блок, достаточно использовать внутри него yield — компилятор поймёт, что нужно сделать. Чтобы сделать это более очевидным, можно объявить фиктивный параметр блока: он указывается последним и предваряется амперсандом (&):
def twice(&block)
yield
yield
end
Чтобы вызвать метод и передать ему блок, используйте do ... end или { ... }. Все эти варианты равнозначны:
twice() do
puts "Hello!"
end
twice do
puts "Hello!"
end
twice { puts "Hello!" }
Разница между do ... end и { ... } заключается в том, что do ... end связывается с самым левым вызовом, а { ... } — с самым правым:
foo bar do
something
end
# The above is the same as
foo(bar) do
something
end
foo bar { something }
# The above is the same as
foo(bar { something })
Это позволяет создавать предметно-ориентированные языки (DSL) с помощью do ... end, чтобы код читался как обычный английский текст:
open file "foo.cr" do
something
end
# Same as:
open(file("foo.cr")) do
end
Вряд ли вы захотите, чтобы приведённый выше код выглядел так:
open(file("foo.cr") do
end)
Перегрузки
Два метода — один с yield, а другой без него — считаются разными перегрузками, как объясняется в разделе перегрузка.
Аргументы yield
Выражение yield похоже на вызов и может принимать аргументы. Например:
def twice
yield 1
yield 2
end
twice do |i|
puts "Got #{i}"
end
Приведённый выше код выводит «Got 1» и «Got 2».
Также можно использовать фигурные скобки:
twice { |i| puts "Got #{i}" }
Можно передать yield несколько значений:
def many
yield 1, 2, 3
end
many do |x, y, z|
puts x + y + z
end
# Output: 6
Блок может указывать меньше параметров, чем передано аргументов:
def many
yield 1, 2, 3
end
many do |x, y|
puts x + y
end
# Output: 3
Указывать в блоке больше параметров, чем передано аргументов, нельзя:
def twice
yield
yield
end
twice do |i| # Error: too many block parameters
end
Каждый параметр блока имеет тип, определяемый всеми выражениями yield в соответствующей позиции. Например:
def some
yield 1, 'a'
yield true, "hello"
yield 2, nil
end
some do |first, second|
# first is Int32 | Bool
# second is Char | String | Nil
end
В качестве параметра блока также можно использовать подчёркивание:
def pairs
yield 1, 2
yield 2, 4
yield 3, 6
end
pairs do |_, second|
print second
end
# Output: 246
Краткий синтаксис с одним параметром
Если у блока один параметр и для него вызывается метод, блок можно заменить аргументом краткого синтаксиса.
Этот код:
method do |param|
param.some_method
end
и
method { |param| param.some_method }
можно записать так:
method &.some_method
Или так:
method(&.some_method)
В обоих случаях &.some_method — это аргумент, передаваемый в method. Синтаксически этот аргумент эквивалентен вариантам с блоком. Это всего лишь синтаксический сахар, не влияющий на производительность.
Если у метода есть другие обязательные аргументы, аргумент краткого синтаксиса тоже следует указать в списке аргументов метода.
["a", "b"].join(",", &.upcase)
Эквивалентно следующему:
["a", "b"].join(",") { |s| s.upcase }
Аргументы можно использовать и с аргументом краткого синтаксиса:
["i", "o"].join(",", &.upcase(Unicode::CaseOptions::Turkic))
Можно вызывать и операторы:
method &.+(2)
method(&.[index])
Значение yield
Само выражение yield имеет значение — значение последнего выражения блока. Например:
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
i + 1
end
Приведённый выше код выводит «2» и «3».
Значение выражения yield особенно полезно для преобразования и фильтрации значений. Лучшие примеры — Enumerable#map и Enumerable#select:
ary = [1, 2, 3]
ary.map { |x| x + 1 } # => [2, 3, 4]
ary.select { |x| x % 2 == 1 } # => [1, 3]
Пример метода-преобразования:
def transform(value)
yield value
end
transform(1) { |x| x + 1 } # => 2
Результат последнего выражения — 2, потому что последнее выражение метода transform — это yield, значение которого совпадает со значением последнего выражения блока.
Ограничения типов
Тип блока в методе, использующем yield, можно ограничить с помощью синтаксиса &block. Например:
def transform_int(start : Int32, &block : Int32 -> Int32)
result = yield start
result * 2
end
transform_int(3) { |x| x + 2 } # => 10
transform_int(3) { |x| "foo" } # Error: expected block to return Int32, not String
break
Выражение break внутри блока досрочно завершает метод:
def thrice
puts "Before 1"
yield 1
puts "Before 2"
yield 2
puts "Before 3"
yield 3
puts "After 3"
end
thrice do |i|
if i == 2
break
end
end
Приведённый выше код выводит «Before 1» и «Before 2». Метод thrice не выполнил выражение puts "Before 3" из-за break.
break также может принимать аргументы: они становятся возвращаемым значением метода. Например:
def twice
yield 1
yield 2
end
twice { |i| i + 1 } # => 3
twice { |i| break "hello" } # => "hello"
Значение первого вызова — 3, потому что последнее выражение метода twice — это yield, которому присваивается значение блока. Значение второго вызова — «hello», потому что был выполнен break.
Если выход из блока выполняется условно, тип возвращаемого значения вызова будет объединением типа значения блока и типов значений всех break:
value = twice do |i|
if i == 1
break "hello"
end
i + 1
end
value # :: Int32 | String
Если break получает несколько аргументов, они автоматически преобразуются в Tuple:
values = twice { break 1, 2 }
values # => {1, 2}
Если break не получает аргументов, это равнозначно передаче одного аргумента nil:
value = twice { break }
value # => nil
next
Выражение next внутри блока досрочно завершает блок, но не метод. Например:
def twice
yield 1
yield 2
end
twice do |i|
if i == 1
puts "Skipping 1"
next
end
puts "Got #{i}"
end
# Output:
# Skipping 1
# Got 2
Выражение next принимает аргументы, которые задают значение выражения yield, вызвавшего блок:
def twice
v1 = yield 1
puts v1
v2 = yield 2
puts v2
end
twice do |i|
if i == 1
next 10
end
i + 1
end
# Output
# 10
# 3
Если next получает несколько аргументов, они автоматически преобразуются в Tuple. Если аргументов нет, это равнозначно передаче одного аргумента nil.
with ... yield
Выражение yield можно изменить с помощью ключевого слова with, чтобы указать объект, который будет получателем по умолчанию для вызовов методов внутри блока:
class Foo
def one
1
end
def yield_with_self
with self yield
end
def yield_normally
yield
end
end
def one
"one"
end
Foo.new.yield_with_self { one } # => 1
Foo.new.yield_normally { one } # => "one"
Распаковка параметров блока
Для параметра блока можно указать вложенные параметры в круглых скобках:
array = [{1, "one"}, {2, "two"}]
array.each do |(number, word)|
puts "#{number}: #{word}"
end
Это всего лишь синтаксический сахар для следующей записи:
array = [{1, "one"}, {2, "two"}]
array.each do |arg|
number = arg[0]
word = arg[1]
puts "#{number}: #{word}"
end
Это означает, что параметр блока можно распаковать, если тип поддерживает вызов [] с целыми числами.
Для параметров типа Tuple можно воспользоваться автоматической распаковкой аргументов и обойтись без круглых скобок:
array = [{1, "one", true}, {2, "two", false}]
array.each do |number, word, bool|
puts "#{number}: #{word} #{bool}"
end
Hash(K, V)#each передаёт блоку Tuple(K, V), поэтому при итерации по парам ключ-значение работает автоматическая распаковка аргументов:
h = {"foo" => "bar"}
h.each do |key, value|
key # => "foo"
value # => "bar"
end
Производительность
При использовании блоков с yield блоки всегда встраиваются в код: замыкания, вызовы и указатели на функции не используются. Это означает, что следующий код:
def twice
yield 1
yield 2
end
twice do |i|
puts "Got: #{i}"
end
полностью эквивалентен такой записи:
i = 1
puts "Got: #{i}"
i = 2
puts "Got: #{i}"
Например, стандартная библиотека содержит метод times для целых чисел, позволяющий написать:
3.times do |i|
puts i
end
Выглядит очень изящно, но работает ли это так же быстро, как цикл for в C? Ответ: да!
Вот определение Int#times:
struct Int
def times
i = 0
while i < self
yield i
i += 1
end
end
end
Поскольку блок без захвата всегда встраивается в код, приведённый выше вызов метода полностью эквивалентен следующей записи:
i = 0
while i < 3
puts i
i += 1
end
Не бойтесь использовать блоки для повышения читаемости или повторного использования кода — это не повлияет на производительность полученного исполняемого файла.
To the extent possible under law, the persons who contributed to this workhave waived
all copyright and related or neighboring rights to this workby associating CC0 with it.
https://crystal-lang.org/reference/1.2/syntax_and_semantics/blocks_and_procs.html