Понимание Ruby Блоков, Проков и лямбда-выражений.

Robert Sosinski, “Understanding Ruby Blocks, Procs and Lambdas”, public translation into Russian from English More about this translation.

Translate into another language.

Блоки, Проки и лямбда-выражения (так же называемые "замыканиями") -- одни из самых мощных возможностей языка Ruby, также и одни из самых сложных для правильного понимания, так как замыкания в ruby реализуются по-своему. Ruby поддерживает четыре (не всегда очевидных) пути создания замыканий. В Сети сложно найти хороший материал по этому вопросу. Надеюсь это руководство поможет вам разобраться с замыканиями в ruby.

Начнем с Блоков

Самое частое, простое и наверное, наиболее полно соответствующее "Пути Ruby" -- использование замыканий с блоками.
Знакомый вам синтаксис:

array = [1, 2, 3, 4]

array.collect! do |n|
n ** 2
end

puts array.inspect # => [1, 4, 9, 16]

Что мы сделали?

1. Массиву "array" послали метод "collect!" с блоком кода.

2. В каждой итерации блок кода возводит в квадрат переменную метода "collect!" (в данном случае n).

3. Все элементы массива возведены в квадрат.

Использовать блоки совместно с методом "collect!" очень легко -- мы просто должны понимать что метод "collect!" для каждой переменной массива выполняет код в блоке. Ну а что если мы захотим опрелелить собственный метод "collect!"? На что он будет похож?
Давайте создадим метод "iterate!" и посмотрим.

class Array

def iterate!

self.each_with_index do |n, i|

self[i] = yield(n)
#для элемента массива (n) по индексу "объект[i]" выполнить блок кода "yield"

end
end
end

array = [1, 2, 3, 4]

array.iterate! do |n|
n ** 2
end

puts array.inspect # => [1, 4, 9, 16]

Вначале мы добавили в класс "Array" метод "iterate!". Согласно принятым в ruby соглашением, мы поставили в конце названия метода восклицательный знак, чтобы предупредить о потенциальной опасности этого метода (он изменяет данные по месту, а не возвращает их). Далее мы воспользовались нашим методом также как и встроенным "collect!". Однако все интересное в середине определения метода.

В отличие от атрибутов, вам не нужно указывать название блоков в своих методах. Вместо этого используется ключевое слово "yield". Использование этого ключевого слова приводит к выполнению кода внутри блока, переданного в метод. Обратите внимание как в yield передается n (целое, с которым сейчас работает метод each_with_index). Передаваемые в yield атрибуты соответствуют переменным в списке, ограниченным вертикальными чертами. Такое значение доступно блоку и возвращается вызовом yield. Резюмируем, что происходит:

1. Послать "iterate!" массиву чисел.

2. Когда "yield" вызван для числа "n" (сначала 1, потом 2 и т.д.), выполнить переданный блок кода.

3. Блок имеет доступное число (также назваемое n) и квадрат этого числа. Так как это последнее значение в блоке, то оно и возвращается автоматически.

4. Yield выводит значение, возвращенное блоком, и перезаписывает значение в массиве.

5. Всё повторяется для каждого элемента в массиве.

Итак, мы сейчас имеем гибкий способ взаимодействия с нашим методом. Считайте, что блоки предоставляют нашему методу некий API, в котором можно определить возведение каждого элемента массива в квадрат, в куб или преобразование каждого числа в строку и вывод их на экран. Вариантов бесконечно много, что позволяет делать метод очень гибким и, по сути, очень мощным.

Но это только начало. Использование yield -- это только один способ применения блоков кода. Есть и другие способы. Например, так называемые Proc. Взглянем на них.

class Array
def iterate!(&code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array = [1, 2, 3, 4]

array.iterate! do |n|
n ** 2
end

puts array.inspect # => [1, 4, 9, 16]

Выглядит очень похоже на предыдущий пример, но здесь есть два отличия. Мы передаем амперсанд с аргументом &code. Этот аргумент, очевидно, наш блок. И также внутри метода iterate! вместо использования yield происходит вызов блока кода. Результат получается аналогичный. Но зачем тогда такое различие в синтаксисе? Это позволит нам больше узнать о сущности блоков. Взглянем:

def what_am_i(&block)

block.class

end

puts what_am_i {}

# => Proc

Блок -- это просто Proc! Тогда, что такое Proc?

Процедуры, они же Proc'и

Блоки очень удобны и синтаксически просты, однако мы можем захотеть иметь много разных блоков, на наше усмотрение, используя их множество раз.

class Array
def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array_1 = [1, 2, 3, 4]
array_2 = [2, 3, 4, 5]

square = Proc.new do |n|
n ** 2
end

array_1.iterate!(square)
array_2.iterate!(square)

puts array_1.inspect
puts array_2.inspect

# => [1, 4, 9, 16]
# => [4, 9, 16, 25]

Почему block в нижнем регистре, а Proc в верхнем?

Я всегда пишу Proc, заглавными, так как это собственный класс в Ruby. Тем не менее, у блоков нет класса (они просто Procs в конце концов), и являются только типом синтаксиса в Ruby. Таким образом, я пишу блок в нижнем регистре. Позднее в этом руководстве, вы увидите, что лямбда я пишу тоже в нижнем регистре по той же причине.

Обратите внимание, что мы не указываем & в атрибуте "code" метода "iterate!", так как вычисление Procs не отличается от passing других типов данных. Так как Проки это объекты, мы можем делать интересные шутки с Руби:


class Array
def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array = [1, 2, 3, 4]

array.iterate!(Proc.new do |n|
n ** 2
end)

puts array.inspect

# => [1, 4, 9, 16]

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

Встает вопрос почему бы не использовать только блоки? Ответ прост, что если мы захотим передать два или более замыканий в метод? Если это потребуется блоки ограничены в использовании. Однако используя Proc, мы можем сделать что-то подобное:

def callbacks(procs)
procs[:starting].call

puts "Still going"

procs[:finishing].call
end

callbacks(:starting => Proc.new { puts "Starting" },
:finishing => Proc.new { puts "Finishing" })

# => Starting
# => Still going
# => Finishing

Итак, когда вы должны использовать блоки вместо Proc? Моя логика следующая:

1. Block: Your method is breaking an object down into smaller pieces, and you want to let your users interact with these pieces.

2. Block: You want to run multiple expressions atomically, like a database migration.

3. Proc: Вы хотите использовать блок кода несколько раз.

4. Proc: Ваш метод будет иметь один или боле колбэков.

Лямбды

So far, you have used Procs in two ways, passing them directly as an attribute and saving them as a variable. These Procs act very similar to what other languages call anonymous functions, or lambdas. To make things more interesting, lambdas are available within Ruby too. Take a look:

class Array
def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

array = [1, 2, 3, 4]

array.iterate!(lambda { |n| n ** 2 })

puts array.inspect

# => [1, 4, 9, 16]

On first look, lambdas seem to be exactly the same as Procs. However, there are two subtle differences. The first difference is that, unlike Procs, lambdas check the number of arguments passed.

def arguments(code)
one, two = 1, 2
code.call(one, two)
end

arguments(Proc.new { |a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}" })

arguments(lambda { |a, b, c| puts "Give me a #{a} and a #{b} and a #{c.class}" })

# => Give me a 1 and a 2 and a NilClass
# *.rb:8: ArgumentError: wrong number of arguments (2 for 3) (ArgumentError)

We see with the Proc example, extra variables are set to nil. However with lambdas, Ruby throws an error instead.

The second difference is that lambdas have diminutive returns. What this means is that while a Proc return will stop a method and return the value provided, lambdas will return their value to the method and let the method continue on. Confused? Lets take a look at an example.

def proc_return
Proc.new { return "Proc.new"}.call
return "proc_return method finished"
end

def lambda_return
lambda { return "lambda" }.call
return "lambda_return method finished"
end

puts proc_return
puts lambda_return

# => Proc.new
# => lambda_return method finished

In proc_return, our method hits a return keyword, stops processing the rest of the method and returns the string Proc.new. On the other hand, our lambda_return method hits our lambda, which returns the string lambda, keeps going and hits the next return and outputs lambda_return method finished. Why the difference?

The answer is in the conceptual differences between procedures and methods. Procs in Ruby are drop in code snippets, not methods. Because of this, the Proc return is the proc_return method’s return, and acts accordingly. Lambdas however act just like methods, as they check the number of arguments and do not override the calling methods return. For this reason, it is best to think of lambdas as another way to write methods, an anonymous way at that.

So, when should you write an anonymous method (lambda) instead of a Proc? The following code shows one such case.

def generic_return(code)
code.call
return "generic_return method finished"
end

puts generic_return(Proc.new { return "Proc.new" })
puts generic_return(lambda { return "lambda" })

# => *.rb:6: unexpected return (LocalJumpError)
# => generic_return method finished

Part of Ruby’s syntax is that arguments (a Proc in this example) cannot have a return keyword in it. However, a lambda acts just like a method, which can have a literal return, and thus sneaks by this requirement unscathed! This different in semantics shows up in situations like the following example.

def generic_return(code)
one, two = 1, 2
three, four = code.call(one, two)
return "Give me a #{three} and a #{four}"
end

puts generic_return(lambda { |x, y| return x + 2, y + 2 })

puts generic_return(Proc.new { |x, y| return x + 2, y + 2 })

puts generic_return(Proc.new { |x, y| x + 2; y + 2 })

puts generic_return(Proc.new { |x, y| [x + 2, y + 2] })

# => Give me a 3 and a 4
# => *.rb:9: unexpected return (LocalJumpError)
# => Give me a 4 and a
# => Give me a 3 and a 4

Here, our method generic_return is expecting the closure to return two values. Doing this without the return keyword becomes dicy though. With a lambda, everything is easy. However with a Proc, we ultimately have to take advantage how Ruby interprets Arrays with assignment.

Итак, когда использовать "Proc" а когда "lambda"? Честно говоря, разница, кроме проверки аргументов состоит только в том, как вы представляете "замыкания". Если "замыкания" для вас -- вычисление блоков кода, используйте "Proc". Если вам больше нравится посылать методу другой метод, который может вернуть метод, используйте "lambda".
Но если lambda -- всего лишь методы объекта, можем ли мы сохранять существующие методы и вычислять их как "Proc"? У ruby припасен хороший трюк для этой цели.

Метод объектов

Итак, у вас есть работающий метод,

class Array

def iterate!(code)
self.each_with_index do |n, i|
self[i] = code.call(n)
end
end
end

def square(n)
n ** 2
end

array = [1, 2, 3, 4]
array.iterate!(method(:square))
puts array.inspect # => [1, 4, 9, 16]

В этом примере мы имеем метод square,

def square(n)
n ** 2
end

puts method(:square).class # => Method

Just as you guessed, square is not a Proc, but a Method. The neat thing is that this Method object will act just like a lambda, because the concept is the same. This method however, is a named method (called square) while lambdas are anonymous methods.

Заключение

So to recap, we went through Ruby’s four closure types, blocks, Procs, lambdas and Methods. We also know that blocks and Procs act like drop-in code snippets, while lambdas and Methods act just like methods. Finally, through a slew of code examples, you were able to see when to use which and how to use each effectively. Now, you should be able to start using this expressive and interesting feature of Ruby in your own code, and start offering flexible and powerful methods to other developers you work with.

© Copyright © 2010 Robert Sosinski

Original (English): Understanding Ruby Blocks, Procs and Lambdas

Translation: © alexbaumgertner, and_rew, deeper4k, alex_fff, impedance .

translated.by crowd

Like this translation? Share it or bookmark!