Узнайте о блоках, процедурах и лямбда-объектах

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

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

Примечание. Мы подробнее остановимся на закрытии и увидим это на практике в конце этой статьи.

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

Звучит сложно? Не волнуйтесь, мы рассмотрим несколько примеров, и все это обретет смысл через несколько минут.

Блоки

Самое простое возможное определение блока - это фрагмент кода, созданный для последующего выполнения. Он заключен в фигурные скобки {} или ключевые слова do и end. Если вы когда-либо использовали методы #each или #map, поздравляю, вы уже знаете, как использовать блоки!

Вы также можете думать о блоке как о теле метода. Как и метод, он может принимать несколько параметров, которые определяются между двумя вертикальными символами, например: |argument1, argument2, ...|.

Давайте продолжим и рассмотрим пример, чтобы прояснить ситуацию. Сосчитаем до трех с помощью блоков!

# Form 1: multi-line blocks
[1,2,3].each do |n|
  puts n
end
# Form 2: single line blocks
[1,2,3].each { |n| puts n }
# both of these blocks will return the following:
# 1
# 2
# 3

Как видите, в обеих формах n - это параметр, отправляемый в тело блока, которое в нашем случае выводит его на стандартный вывод с помощью puts.

Явные вызовы блокировки

Если вы хотите передать блок в свой метод, у вас есть два варианта: явный или неявный метод. Начнем с явного метода. Несмотря на то, что синтаксис может быть немного странным, концепция проста. Давайте посмотрим на пример, и я объясню его позже.

def my_method(&block)
  block.call
end
my_method { puts 'Hello World!' }
# Hello World!

Здесь мы отправляем блок { puts 'Hello World!' } в качестве явного аргумента нашему методу. Затем внутри нашего метода мы можем выполнить этот метод, вызвав block.call.

Здесь следует обратить внимание на три вещи.

1) Если вы вызовете этот метод, не передав ему блок, вы получите ArgumentError, например:

def my_method(&block)
  block.call
end
my_method('Hello World!')
# ArgumentError: wrong number of arguments (given 1, expected 0)

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

2) &block - это всего лишь соглашение, вы можете использовать любое имя аргумента для хранения вашего блока внутри вашего метода.

def my_method(&my_sweet_block_name)
  my_sweet_block_name.call
end
my_method { puts 'Hello World!' }
# Hello World!

3) Вы можете выполнять свой блок сколько угодно раз внутри своего метода, вам просто нужно call его снова.

def my_method(&block)
  block.call  
  block.call
  block.call
end
my_method { puts 'Hello World!' }
# Hello World!
# Hello World!
# Hello World!

Неявные вызовы блокировки

Неявная передача block работает путем вызова ключевого слова yield внутри вашего метода. Ключевое слово yield - это специальное слово, которое находит и вызывает пройденный блок, поэтому вам не нужно добавлять блок в список аргументов с помощью &block или явно вызывать его с помощью block.call.

def my_method
  yield
end
my_method { puts 'Hello World!' }
# Hello World!

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

Примечание. Вы также можете многократно вызывать свой блок с помощью нескольких yield вызовов

Возможно, вы видели этот тип вызова block внутри файлов представления ваших проектов Rails как комбинацию методов content_for и yield, и теперь вы точно понимаете, что они делают.

Использование параметров блока

Как и метод, вы можете отправлять параметры в вызовы блока. Вот как это сделать на очевидном примере, используя оба описанных выше метода. Явная версия:

def explicit_version(&block)
  block.call(1)
  block.call(2)
  block.call(3)
end
explicit_version { |number| puts number * 2 }
# 2
# 4
# 6

Неявная версия:

def implicit_version
  yield 1
  yield 2
  yield 3
end
implicit_version { |number| puts number * 2 }
# 2
# 4
# 6

Procs (сокращенно от процедуры)

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

proc = Proc.new { puts "Hello World!" }
proc.call
# Hello World!

Одним из преимуществ использования proc является то, что вы можете отправлять несколько блоков в качестве аргументов метода, как и обычные параметры. Нравится:

def my_method(print_hello, print_number)
  print_hello.call
  print_number.call(2)
end
print_hello = Proc.new { puts "Hello World!" }
print_number = Proc.new { |n| puts n }
my_method(print_hello, print_number)
# Hello World
# 2

: to_proc

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

{ a: 1, b: 2 }.to_proc
#<Proc:0x00007fe63292f4a8>
:a.to_proc
#<Proc:0x00007fe63296e360(&:a)>
def my_method
  puts 'Hello World!'
end.to_proc
#<Proc:0x00007fe63293e4a8(&:my_method)>

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

Вероятно, вы раньше неявно использовали метод to_proc в сочетании с map, например:

[1,2,3].map(&:to_s)
# ["1", "2", "3"]

В основном мы здесь отправляем блок нашему map методу, используя явную нотацию (с амперсандом). В этом случае наш блок - это символ :to_s, который автоматически преобразуется в процесс с помощью своего #to_proc метода. После преобразования блок вызывается методом call, как мы видели раньше.

Итак, вот наш последний фрагмент кода после вызова to_proc, чтобы прояснить ситуацию:

[1,2,3].map { |i| i.to_s }
# ["1", "2", "3"]

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

Лямбды

лямбда - это особый тип proc. Давайте посмотрим, как мы можем его объявить.

say_something = -> { puts 'This is a lambda' }
say_something.call
# This is a lambda

Пока что это почти то же самое, что и объект proc, за исключением синтаксиса, но объект lambda намного больше похож на обычный метод, чем на proc есть. Давайте взглянем на несколько ключевых отличий.

1) Как и обычный метод, объект lambda выдает ошибку, если вы вызываете его с неправильным числом аргументов, но proc объект не будет.

print_n = lambda { |n| puts "You will not see this message" }
print_n.call
# ArgumentError: wrong number of arguments (given 0, expected 1)
print_n = Proc.new { |n| puts "You will see this message" }
print_n.call
# You will see this message

2) Объект лямбда будет возвращаться нормально, как и метод, но proc вернется из своего текущего контекста. Вот пример, начиная с поведения proc:

def call_proc
  puts "This will be printed" 
  my_proc = Proc.new { return 1 } 
  my_proc.call 
  puts "This will not be printed"
end
call_proc
# This will be printed
# 1

И вот что происходит, когда возвращается лямбда:

def call_lambda 
  puts "This will be printed" 
  my_proc = -> { return 1 } 
  my_proc.call 
  puts "This will also be printed"
end
call_lambda
# This will be printed
# This will also be printed

Итак, вот краткое изложение основных различий между lambdas и procs:

  • Лямбды определяются с помощью -> {} или lambda {}, а procs с Proc.new {};
  • Procs возвращаются из текущего контекста, а lambdas возвращаются точно так же, как и метод;
  • Procs не заботится о правильном количестве аргументов, тогда как lambdas вызовет исключение, если вызывается с неправильным числом аргументов.

Закрытие

Ruby procs и lambdas имеют еще один специальный атрибут. Когда они определены, они будут нести с собой текущую область видимости, как локальные переменные и методы из контекста, в котором они были созданы.

Это не означает, что они будут нести с собой значения переменных, но на самом деле это ссылка - или указатель для любителей C. Поэтому, если значение переменной изменится после определения proc, у них всегда будет с собой последняя версия.

Давайте посмотрим на пример, чтобы попытаться понять это:

def my_method(my_proc)
  count = 3
  my_proc.call
end 
count = 1
my_proc = Proc.new { puts count }
count = 2
puts my_method(my_proc)
# 2

Это может показаться немного нелогичным, но если вы присмотритесь, то увидите, что это имеет смысл. Несмотря на то, что proc был объявлен, когда count был 1, поскольку он имеет ссылку на count, он был автоматически обновлен, когда мы написали counter = 2. Кроме того, proc проигнорировал строку count = 3, потому что, поскольку это закрытие, переменные в нем уже сохранены.

Примечание. Это отличный вопрос для собеседования с Ruby, не так ли?

Резюме

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

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

И это все, что нужно для этой статьи! Надеюсь, вам понравилось, и что это не слишком запутало. Если у вас есть вопросы, не стесняйтесь спрашивать меня! Я планирую написать еще одну статью как продолжение этой, с реальными примерами замыканий в Ruby, так что следите за обновлениями!

Кроме того, если вы разработчик рельсов, вот некоторые из моих других статей, которые могут вас заинтересовать: