Spec-Zone.ru › Crystal

Блоки и 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

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API