В 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 г.