Узнайте о блоках, процедурах и лямбда-объектах
В языках программирования с функциями первого класса функции могут храниться в переменных, передаваться в качестве аргументов другим функциям и даже передаваться в качестве возвращаемых аргументов. Первоклассные функции необходимы для стиля функционального программирования.
закрытие - это первоклассная функция с окружением. Среда - это отображение, связывающее каждую переменную функции со значением, к которому было привязано имя при создании замыкания.
Примечание. Мы подробнее остановимся на закрытии и увидим это на практике в конце этой статьи.
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, так что следите за обновлениями!
Кроме того, если вы разработчик рельсов, вот некоторые из моих других статей, которые могут вас заинтересовать: