В Ruby Magic нам нравится погружаться в магию вещей, которые мы используем каждый день, чтобы понять, как они работают. В этом выпуске мы исследуем различия между блоками, процедурами и лямбдами.

В языках программирования с функциями первого класса функции могут храниться в переменных и передаваться в качестве аргументов другим функциям. Функции могут даже использовать другие функции в качестве возвращаемых значений.

Замыкание - это первоклассная функция с окружением. Среда - это отображение переменных, существовавших при создании замыкания. Замыкание сохранит доступ к этим переменным, даже если они определены в другой области.

В Ruby нет первоклассных функций, но есть замыкания в виде блоков, процедур и лямбда-выражений. Блоки используются для передачи блоков кода методам, а процедуры и лямбда-выражения позволяют хранить блоки кода в переменных.

Блоки

В Ruby блоки - это фрагменты кода, которые можно создать для последующего выполнения. Блоки передаются методам, которые выдают их в рамках ключевых слов do и end. Одним из многих примеров является метод #each, который перебирает перечислимые объекты.

[1,2,3].each do |n|
  puts "#{n}!"
end

[1,2,3].each { |n| puts "#{n}!" } # the one-line equivalent.

В этом примере блок передается методу Array#each, который запускает блок для каждого элемента в массиве и выводит его на консоль.

def each
  i = 0
  while i < size
    yield at(i)
    i += 1
  end
end

В этом упрощенном примере Array#each в цикле while вызывается yield для выполнения переданного блока для каждого элемента в массиве. Обратите внимание, что этот метод не имеет аргументов, так как блок передается методу неявно.

Неявные блоки и yield ключевое слово

В Ruby методы могут принимать блоки неявно и явно. Неявная передача блока работает путем вызова ключевого слова yield в методе. Ключевое слово yield особенное. Он находит и вызывает переданный блок, поэтому вам не нужно добавлять блок в список аргументов, которые принимает метод.

Поскольку Ruby допускает неявную передачу блоков, вы можете вызывать все методы с блоком. Если он не вызывает yield, блок игнорируется.

irb> "foo bar baz".split { p "block!" }
=> ["foo", "bar", "baz"]

Если вызываемый метод выполняет yield, переданный блок обнаруживается и вызывается с любыми аргументами, которые были переданы ключевому слову yield.

def each
  return to_enum(:each) unless block_given?

  i = 0
  while i < size
    yield at(i)
    i += 1
  end
end

Этот пример возвращает экземпляр Enumerator, если не указан блок.

Ключевые слова yield и block_given? находят блок в текущей области. Это позволяет передавать блоки неявно, но предотвращает прямой доступ кода к блоку, поскольку он не хранится в переменной.

Явно передающие блоки

Мы можем явно принять блок в методе, добавив его в качестве аргумента с помощью параметра амперсанда (обычно называемого &block). Поскольку теперь блок является явным, мы можем использовать метод #call непосредственно для результирующего объекта вместо того, чтобы полагаться на yield.

Аргумент &block не является правильным аргументом, поэтому вызов этого метода с чем-либо другим, кроме блока, приведет к ArgumentError.

def each_explicit(&block)
  return to_enum(:each) unless block

  i = 0
  while i < size
    block.call at(i)
    i += 1
  end
end

Когда блок передается таким образом и сохраняется в переменной, он автоматически преобразуется в proc.

Procs

«Процесс» - это экземпляр класса Proc, который содержит блок кода, который должен быть выполнен, и может быть сохранен в переменной. Чтобы создать процесс, вы вызываете Proc.new и передаете ему блок.

proc = Proc.new { |n| puts "#{n}!" }

Поскольку процесс может быть сохранен в переменной, его также можно передать методу, как обычный аргумент. В этом случае мы не используем амперсанд, поскольку процедура передается явно.

def run_proc_with_random_number(proc)
  proc.call(random)
end

proc = Proc.new { |n| puts "#{n}!" }
run_proc_with_random_number(proc)

Вместо того, чтобы создавать процедуру и передавать ее методу, вы можете использовать синтаксис параметра амперсанда Ruby, который мы видели ранее, и вместо этого использовать блок.

1def run_proc_with_random_number(&proc)
  proc.call(random)
end

run_proc_with_random_number { |n| puts "#{n}!" }

Обратите внимание на добавленный амперсанд к аргументу в методе. Это преобразует переданный блок в объект proc и сохранит его в переменной в области действия метода.

Совет. Хотя в некоторых ситуациях полезно иметь процедуру в методе, преобразование блока в процедуру снижает производительность. По возможности используйте вместо этого неявные блоки.

#to_proc

Символы, хэши и методы могут быть преобразованы в процедуры с помощью их #to_proc методов. Часто это используется для передачи процесса, созданного из символа, в метод.

[1,2,3].map(&:to_s)
[1,2,3].map {|i| i.to_s }
[1,2,3].map {|i| i.send(:to_s) }

В этом примере показаны три эквивалентных способа вызова #to_s для каждого элемента массива. В первом передается символ с префиксом амперсанда, который автоматически преобразует его в процесс, вызывая его метод #to_proc. Последние два показывают, как может выглядеть этот процесс.

class Symbol
  def to_proc
    Proc.new { |i| i.send(self) }
  end
end

Хотя это упрощенный пример, реализация Symbol#to_proc показывает, что происходит под капотом. Метод возвращает процедуру, которая принимает один аргумент и отправляет ему self. Поскольку self является символом в этом контексте, он вызывает метод Integer#to_s.

Лямбды

Лямбды - это, по сути, процессы с некоторыми отличительными факторами. Они больше похожи на «обычные» методы в двух аспектах: они определяют количество аргументов, передаваемых при их вызове, и используют «нормальные» результаты.

При вызове лямбды, которая ожидает аргумент без аргумента, или если вы передаете аргумент лямбде, которая его не ожидает, Ruby выдает ArgumentError.

irb> lambda (a) { a }.call
ArgumentError: wrong number of arguments (given 0, expected 1)
        from (irb):8:in `block in irb_binding'
        from (irb):8
        from /Users/jeff/.asdf/installs/ruby/2.3.0/bin/irb:11:in `<main>'

Кроме того, лямбда обрабатывает ключевое слово return так же, как и метод. При вызове процедуры программа передает управление блоку кода в процедуре. Итак, если процедура возвращается, возвращается текущая область видимости. Если процедура вызывается внутри функции и вызывает return, функция также немедленно возвращается.

def return_from_proc
  a = Proc.new { return 10 }.call
  puts "This will never be printed."
end

Эта функция передаст управление процессу, поэтому, когда она вернется, функция вернется. Вызов функции в этом примере никогда не распечатает результат и вернет 10.

def return_from_lambda
  a = lambda { return 10 }.call
  puts "The lambda returned #{a}, and this will be printed."
end

При использовании лямбда он будет напечатан. Вызов return в лямбда-выражении будет вести себя как вызов return в методе, поэтому переменная a заполняется значением 10, а строка выводится на консоль.

Блоки, проки и лямбды

Теперь, когда мы разобрались с обоими блоками, процедурами и лямбдами, давайте снова уменьшим масштаб и подведем итоги сравнения.

  • Блоки широко используются в Ruby для передачи фрагментов кода функциям. Используя ключевое слово yield, можно неявно передать блок без преобразования его в процедуру.
  • При использовании параметров с префиксом амперсандов передача блока методу приводит к возникновению процедуры в контексте метода. Процедуры ведут себя как блоки, но могут храниться в переменной.
  • Лямбда-выражения - это процедуры, которые ведут себя как методы, что означает, что они обеспечивают арность и возвращаются как методы, а не в своей родительской области.

На этом мы завершаем рассмотрение замыканий в Ruby. Есть еще кое-что, чтобы узнать о замыканиях, таких как лексические области и привязки, но мы сохраним это для будущего выпуска. А пока дайте нам знать, о чем вы хотели бы прочитать в будущих выпусках Ruby Magic, закрытиях и других материалах, на странице @AppSignal.

Первоначально опубликовано на blog.appsignal.com 4 сентября 2018 г.