Les réflexions de l'ingénieur

Замыкания в Ruby: блоки, процедуры, лямбды

Блоки, процедуры и лямбды — замыкания (closures) — являются важным аспектом языка Ruby, с одной стороны, и малопонятным с другой. Малопонятным в основном из-за того, что Ruby подходит к замыканиям в значительной степени уникальным образом, к тому же в Ruby существуют три разных пути использования замыканий.

Хорошо описанных руководств по замыканиям в Ruby не так много, одно из них послужило материалом для данного перевода — Understanding Ruby Blocks, Procs and Lambdas, написанное Робертом Сосински (Robert Sosinski), также он подготовил репозиторий c примерами на GitHub.

Путь Первый - блоки

Самый распространённый, простой и вероятно самый «Ruby like» путь использования замыканий в Ruby - это блоки (blocks). У них очень простой синтаксис:

[1, 2, 3, 4].map { |i| i**3 }

Блоком является выражение внутри {}. В фигурных скобочках записываются однострочные блоки, тоже самое можно записать иначе:

[1, 2, 3, 4].map do |i|	i**3 end

Так записывают блоки состоящие из нескольких строк. Здесь тот же блок только заключённый между do и end. Конечно, можно использовать {} и do-end там и там, но между ними есть небольшая разница [todo: описать разницу]. И обычно поэтому пишут именно так как указано в примерах.

Что же такое блок? Фактически блок — это просто выражение-процедура. В примере метод map для каждого элемента массива [1, 2, 3, 4] выполняет выражение в блоке, где на каждой итерации в блок будет передаваться значение элемента массива, причём это значение будет в переменной n. Имя переменной - произвольное, интерпретатор Ruby использует для этого значение записанное между двумя |.

Как же метод map взаимодействует с блоком? Рассмотрим следующий пример:

class Array	def map		# создадим новый массив		array = []		# в цикле обходим наш массив		for x in self do			# в новый массив добавляем результат выполнения выражения описанного в блоке			array << yield(x)		end		# возвращаем результирующий массив		array	endend

Здесь используется специальная конструкция yiеld, которая выполняет выражение с заданными параметрами и возвращает результат. В примере в блок передаётся переменная x, в которой содержится элемент массива на текущей итерации.

Итак, с одной стороны, yiеld(x) с другой блок { |i| i**3 }, таким образом, на каждой итерации обхода массива yiеld(x) выполняет блок { |i| i**3 } в который будут передаваться значения массива. В примере это числа от 1 до 4, соответственно результатом будет массив кубов этих чисел.

[[Схожим образом дело обстоит в ДССП где процедура в цикле выполняет операции над вершиной сетка.]]

Благодаря блокам Ruby позволяет писать простой и надёжный код, например, нам надо открыть файл, выполнить операции и закрыть, с блоками это очень просто!

File.open('filename') { |f| … }

Внутри, как нетрудно догадаться, будет что-то подобное:

def open(file)	fd = read_file(file)	yield(fd)	fd.closeend

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

Следует отметить, что количество аргументов неограниченно: yiеld(x, y, z) -> |a, b, c|, также аргументов может не быть вовсе.

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

class Array	def map(&our_block)		array = []		for x in self do			array << our_block.call(x)		end		array	endend

Пример, практически, аналогичен предыдущему, однако всё же есть пара различий. Первое, мы передаём аргумент our_block в метод map, который к тому же начинается со знака & (это способ отметить аргумент, который будет соответствовать передаваемому блоку). Второе отличие - это способ вызова блока, теперь вместо yield в нужном месте, вызываем метод call у нашего блока c требуемыми параметрами. Результат будет идентичен предыдущему варианту с yield. Зачем два варианта одного и того же? Давайте узнаем, чем же на самом деле в последнем примере является наш блок.

def some_method(&block)	block.classendputs some_method {}# => Proc

Блок это просто объект Proc! А что есть Proc?

Путь второй - процедуры, они же Proc

Блоки очень удобны и имеют простой синтаксис, однако нам может понадобиться много различных блоков и возможность их многократного использования. Таким образом, передавая один и тот же блок снова и снова в разных местах получим дублирование кода. Однако, так как Ruby полностью объектно-ориентированный язык, он позволяет избежать такой ситуации, сохранив повторяющейся код как класс объектов. Таким классом является Proc (сокращение от Procedure — процедура). Разница между блоком и Proc в том, что блок, хоть и является экземпляром Proc, его нельзя запомнить, блок это решение для однократного использования. Работая же с Procs, мы можем делать следующие:

class Array	def map(block)		array = []		for x in self do			array << block.call(x)		end		array	endendsquare = Proc.new do |i|	i ** 2end[1, 2, 3, 4].map(square) # => [1, 4, 9, 16][5, 6, 7, 8].map(square) # => [25, 36, 49, 64]

Обратите внимание, что в методе map аргумент записывается без &. Это потому, что передача Procs ничем не отличается от передачи любых других данных. Так как экземпляры Proc - это обычные объекты, можно проделывать и такой трюк:

[1, 2, 3, 4].map(Proc.new { |i| i**2 }) # => [1, 4, 9, 16]

Согласитесь, эквивалентная ей следующая запись куда проще:

[1, 2, 3, 4].map { |i| i**2 } # => [1, 4, 9, 16]

Но не надо сбрасывать со счетов Proc. Например, как передать два блока? С простыми блоками — никак. Но создавая Proc очень просто!

def some_method(procs)	procs[:starting].call	puts 'Some operations'	procs[:finishing].callendsome_method(	:starting => Proc.new { puts 'Starting' }, 	:finishing => Proc.new { puts 'Finishing' }	)# => Starting# => Still going# => Finishing

Итак, когда использовать Procs? Procs:

  • требуется использовать повторно блок
  • требуется использовать несколько блоков сразу
  • требуется обернуть код в блок

Для других задач вполне достаточно блоков.

Путь третий - Лямбды (Lambdas)

До этого мы использовали Procs двумя способами, передавая их напрямую как атрибут и сохраняя их в переменной. Это похоже на то, что в других языках называется анонимными функциями или лямбдами (lambdas). Что ещё интереснее, лямбды присутствуют и в Ruby.

class Array	def map(block)		array = []		for x in self do			array << block.call(x)		end		array	endend[1, 2, 3, 4].map(lambda { |i| i ** 2 }) # => [1, 4, 9, 16]

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

def arguments(code)  one, two = 1, 2  code.call(one, two)endarguments(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:10: ArgumentError: wrong number of arguments (2 for 3) (ArgumentError)

Видно, что в примере с Proc, дополнительная переменная просто равна nil. Однако, в случае с лямбдами, Ruby выбрасывает исключение.

Второе различие в том как обрабатывается return. В то время как return в Proc прерывает поток исполнения метода в котором он был вызван, лямбда просто возвращает значение в метод, который продолжает поток исполнения.

def proc_return  Proc.new { return "Proc.new"}.call  return "proc_return method finished"enddef lambda_return  lambda { return "lambda" }.call  return "lambda_return method finished"endputs proc_returnputs lambda_return# => Proc.new# => lambda_return method finished

В proc_return, наш метод выполняет return, останавливая процесс дальнейшего выполнения метода, возвращая строку Proc.new. С другой стороны, lambda_return выполняет код в замыкании, которое возвращает строку lambda, продолжая выполнение основного метода, который возвращает строку lambda_return method finished.

Ответ кроется в концептуальном различии между процедурами и методами. Процедуры (Proc) в Ruby - это фрагмент кода, не метод. Поэтому return в proc_return по сути является return для самого метада, в котором тот был выполнен. Лямбды (lambda) ведут себя как методы, они проверяют количество аргументов и не переопределяют вызов return в методах. Поэтому проще считать лямбды безымянными методами.

Leave a message