Статьи

Странный Ruby Часть 4: Кодовые блоки (блоки, проки и лямбды)

[Эта статья была написана Джонаном Шеффлером]

Это четвертая часть серии о странном рубине. Не пропустите  Weird Ruby Part 1: Начало конца , Weird Ruby Part 2: Exceptional Ensurance и  Weird Ruby Part 3: Веселье с триггером .

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

Давайте начнем с этого простого определения закрытия в кредит из  Википедии :

«В языках программирования замыкание (также лексическое замыкание или замыкание функции) представляет собой функцию или ссылку на функцию вместе со ссылочной средой — таблицей, хранящей ссылку на каждую из нелокальных переменных (также называемых свободными переменными или повышающими значениями). этой функции. Замыкание — в отличие от простого указателя на функцию — позволяет функции получать доступ к этим нелокальным переменным, даже если она вызывается вне ее непосредственной лексической области ».

Я уверен, что есть некоторые из вас, у которых не было проблем с поиском объяснения с первой попытки; к сожалению, я не могу причислить себя к вашему уважаемому членству. Это может быть одним из тех случаев, когда честное стремление быть максимально точным приводит к чрезмерно плотному описанию. Гораздо более простой способ взглянуть на замыкания — это просто переносимый код — крошечные блоки кода, которые вы можете передавать как любой другой объект и выполнять по желанию.

Три варианта закрытия

Замыкания в Ruby настолько важны, что у нас есть 3 варианта: блоки, процы и лямбды.

&block
Proc.new
lambda

Все три из этих конструкций можно было бы назвать «замыканиями», хотя некоторые из них не соответствуют техническому определению (педанты опускают руки, никто не собирается на вас обращаться). Я собираюсь пройтись по каждому из них по порядку и обсудить некоторые их различия, чтобы дать вам лучшее представление о том, как работают замыкания в Ruby, и, надеюсь, мы найдем несколько вещей, которые вас удивят.

Блоки

Блоки могут быть созданы несколькими способами в Ruby:

do 
  #this is a block
end

и

{ #this is a bloc

Эти две версии функционируют абсолютно одинаково (за исключением случаев, когда они не подробны), они даже используют один и тот же байт-код:

Байт-код генерируется при запуске этой строки в irb:
RubyVM::InstructionSequence.compile(”10.times do; 1337807; end”).disassemble

== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>========== 
== catch table 
| catch type: break st: 0002 ed: 0006 sp: 0000 cont: 0006 
|----------------------------------------------------------------------- 
0000 trace 1 ( 1) 
0002 putobject 10 
0004 send <callinfo!mid:times, argc:0, block:block in <compiled>> 
0006 leave 
== disasm: <RubyVM::InstructionSequence:block in <compiled>@<compiled>>= 
== catch table 
| catch type: redo st: 0002 ed: 0006 sp: 0000 cont: 0002 
| catch type: next st: 0002 ed: 0006 sp: 0000 cont: 0006 
|----------------------------------------------------------------------- 
0000 trace 256 ( 1) 
0002 trace 1 
0004 putobject 1337807 
0006 trace 512 
0008 leave

Байт-код генерируется при запуске этой строки в irb: 
RubyVM::InstructionSequence.compile(”10.times { 1337807 }”).disassemble

== disasm: <RubyVM::InstructionSequence:<compiled>@<compiled>>========== 
== catch table 
| catch type: break st: 0002 ed: 0006 sp: 0000 cont: 0006 
|----------------------------------------------------------------------- 
0000 trace 1 ( 1) 
0002 putobject 10 
0004 send <callinfo!mid:times, argc:0, block:block in <compiled>> 
0006 leave 
== disasm: <RubyVM::InstructionSequence:block in <compiled>@<compiled>>= 
== catch table 
| catch type: redo st: 0002 ed: 0006 sp: 0000 cont: 0002 
| catch type: next st: 0002 ed: 0006 sp: 0000 cont: 0006 
|----------------------------------------------------------------------- 
0000 trace 256 ( 1) 
0002 trace 1 
0004 putobject 1337807 
0006 trace 512
0008 leave

В сообществе Ruby многострочные блочные тела обычно содержатся внутри  do; end блока, а  {} синтаксис зарезервирован для однострочных тел. Альтернатива этому соглашению была предложена покойным  Джимом Вейрихом  (мы любим тебя, Джим, спасибо тебе за все) и, возможно, другими до и после него, но я знаю это как соглашение Джима по блоку.

Джим предложил, чтобы в блоке с возвращаемым значением, которое вы планировали использовать (функциональный блок), использовался синтаксис фигурных скобок, а в блоке, который вызывал только выходные или побочные эффекты (процедурный блок), использовался синтаксис do-end. Это позволяет сразу определить назначение блока, что, безусловно, звучит полезно.

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

Как я показал ранее, эти два синтаксиса идентичны с одним исключением; их приоритет.

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

def first(arg = nil)
  yield('first') if block_given?
end
def second(arg = nil)
  yield('second') if block_given?
end
# This block will be passed to first
first second do |method_name|
  puts method_name
end
# => first
# This block will be passed to second
first second { |method_name| puts method_name }
# => second

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

Передача аргументов в блоки

Когда вы передаете аргумент в блок, вы оборачиваете входящие аргументы  | следующим образом:

launch { |pod| pod.release }

Теперь вы можете вызвать yield с аргументом внутри метода запуска, и он будет передан блоку в качестве аргумента с именем pod:

def launch   
  yield(Pod.new) 
end

Вы можете включить несколько аргументов так же, как при вызове метода, или можете полностью опустить аргументы, как мы обычно делаем с Integer # times:

10.times do   
  puts “Preparing to launch...”
end

В блоке выше действительно есть аргумент, счетчик для нашего цикла, как вы можете видеть в нижележащем коде C:

for (i=0; i<end; i++) {     
    rb_yield(LONG2FIX(i)); 
}

Если вы хотите ссылку на этот счетчик внутри Integer # раз, вы можете ссылаться на аргумент следующим образом:

10.times do |count| 
  puts “Preparing to launch in #{10 - count}” 
end

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

pod = Proc.new do |missiles, lasers| 
  puts “Missiles loaded” if missiles 
  puts “Lasers charged” if lasers
end

call() Ничего не печатает и не вызывает ошибку, даже если мы договорились передать два аргумента , а вместо этого не передать ни.

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

Как и блоки, процесс может быть создан несколькими способами:

Proc.new { surprise_nuke } 
proc { surprise_nuke }

Эти примеры функционально идентичны в современных версиях Ruby, но  procсинтаксис в версиях Ruby до 1.9 фактически создал вместо этого лямбду, просто для того, чтобы никто на Земле никогда не мог держать это прямо в голове. Если вы время от времени работаете в более старых версиях Ruby, возможно, стоит избегать использования более старого синтаксиса и просто использовать Proc.new.

Как я показал ранее, процы не строги относительно арности (количества аргументов) и не являются блоками Напротив, наш другой вид кода pod, лямбда, очень заботится об арности:

codepod = lambda { |promise| puts “I #{promise}, I’ll never die.” }
codepod.call()
ArgumentError: wrong number of arguments (0 for 1)

Our code pod raises an error when we call it without fulfilling our promise to provide one argument. It will raise a similar error if we pass too many arguments:

codepod = lambda { |promise| puts “I #{promise}, I’ll never die.” }
codepod.call(true, false) 
ArgumentError: wrong number of arguments (2 for 1)

If you tell your lambda that it takes a specific number of arguments it expects you to follow through on that promise. None of this “take any arguments I want” sort of proc tomfoolery.

Astute viewers may also have noticed that lambdas share a class with their rebellious siblings the procs; Lambda is not a class of object in Ruby.

Once again the C code gives us a hint as to how this works:

proc_new(VALUE klass, int8_t is_lambda)

All of the methods that generate procs and lambdas use this function: proc_new. When you want your proc to have lambda behavior you pass TRUE to proc_new and you get a lambda. If you want one of those crazy procs that don’t seem to care what you do, just pass FALSE. The int8_t is how C programmers type Boolean, both because they like to look 1337 and they don’t actually have a Boolean type. TRUE and FALSE in this case are actually just constants defined to be 1 and 0, and int8_t is a single byte representation of those values.

There is one other very important difference introduced by setting is_lambda to TRUEhere: the resulting lambda will return from it’s own context. Procs will return from the surrounding context instead.

Returning from pods

The easiest way to illustrate this difference in return behavior is by creating each of them without any surrounding context:

labamba = lambda { return } 
labamba.call 
=> nil
proctor = Proc.new { return } 
proctor.call 
=> LocalJumpError: unexpected return

The proc created the return in the same context where the proc itself was created, so calling return within a proc is essentially the same as typing return alone and trying to run it.

return => LocalJumpError: unexpected return

Returns need friends, they need something to return from. A return inside a method will return from that method, so a proc created inside that method will also return from that method.

def lawful_good   
  paladin = Proc.new { return }   
  chaotic_evil(paladin)   
  puts “Lawful good!” 
end
def chaotic_evil(recruit)   
  recruit.call   
  puts “Chaotic evil!” 
end
lawful_good 
=> nil

The lawful_good method never runs to completion here, so we never print anything from either method. When paladin is created in the lawful_good method its return is bound forever to returning from that method. No matter where paladin then travels in the world it will immediately fly back to the lawful_good method and execute the return when it’s called. In this example, paladin was called in the middle of the chaotic_evil method in an unsuccessful attempt to convert our noble paladin. The chaotic_evil method stopped executing and so did the lawful_good method, immediately returning nil.

A return inside of a lambda will have a very different result:

def lawful_good   
  paladin = lambda { return }   
  chaotic_evil(paladin)   
  puts “Lawful good!” 
end
def chaotic_evil(recruit)   
  recruit.call   
  puts “Chaotic evil!” 
end
lawful_good
Chaotic evil! 
Lawful good!

The lambda creates its own special snowflake of a context for its return, so callingpaladin really only causes the lambda to return from itself. After the lambda returns nothing, the next line of the chaotic_evil method proclaims, “Chaotic evil!” Fortunately, the lawful_good method will continue executing normally as well, and our noble paladin is once again back with the good team. The lawful_good method proudly announces, “Lawful good!”

In order to illustrate the need for procs to have this type of return behavior, let’s look at the Integer#times method again:

10.times do |count|   
  puts “#{count} little monkey(s) jumping on the bed.” 
end

Remember that the do-end syntax here is creating a block, which is effectively a proc. Each iteration of the loop yields the count to that block and puts our string with the count until we run out of numbers.

Let’s say we have a huge bias against the seventh monkey. That seventh monkey always gets too rowdy and we’re worried one of the other monkeys is going to get hurt.

def monkeys!   
  10.times do |count|     
    return if count == 7    
    puts “#{count} little monkey(s) jumping on the bed.”   
  end 
end

When we get to that return, we expect to return from the monkeys! method that got all of this jumping started in the first place. The block that we created bound the return to the local context and it returns from there, so we will return from monkeys!, ending our shenanigans promptly.

Now let’s pretend that block we’ve created has lambda behavior instead. This is not really an option in Ruby, but that’s why it’s pretend. The return would bind to the context of the lambda we created, and when the count reached seven we would simply return from the lambda itself; we would return from that iteration of our loop. We would get to seven and skip that count moving straight to eight.

Not only is that behavior counterintuitive, it duplicates the behavior of another keyword in Ruby, the next keyword:

10.times do |count| 
  next if count == 7 
  puts “#{count} little monkeys jumping on the bed.” 
end

That’s why blocks have the return behavior that they do; so you can return from a method even if you’re in the middle of your loop. Sometimes you just need to eject, like when you see that seventh monkey coming.

Creating closures

Each of the methods below is a valid means of constructing a closure in Ruby:

Proc.new {} 
proc {} 
lambda {} 
-> {}

The first two methods create procs, though the proc keyword has worked this way only since Ruby 1.9. Before that, proc created a lambda, which rightfully confused everyone who has ever used it.

The second two examples both create lambdas, and the last of these is probably the most popular. The -> syntax was introduced in Ruby 1.9 and is commonly referred to as the stabby lambda, quite an aggressive name for such a cuddly little code pod. I think we should call this the “baby rocket.”

Calling closures

There are a number of ways to call closures in Ruby, some of them clearer than others.

codepod = Proc.new {} 
codepod.call 
codepod() 
codepod.() 
codepod[] 
codepod.[]

Each of these methods for calling a closure is valid, but let’s just agree to stick to call, as this topic is confusing enough. Remember your code can only get so bad before future-you is driven to build a time machine to come back and beat you up.

Review

There are three types of closures in Ruby, but given that blocks and procs have similar behaviors, you can treat them as one. So we have procs and lambdas, differentiated by their arity and return behavior:

  • proc
    • arbitrary arguments
    • returns from home (the method where it was created)
  • lambda
    • strict arguments
    • returns from self

There are some very good reasons why we have these distinct types of closures, and understanding how to use each of them effectively will treat you well along your path to mastering Ruby.

If you have any questions or comments, feel free to reach out: [email protected]. It’s always nice to know I’m not just typing into the void. Also, join the discussion about this topic over at the New Relic Community Forum.

Proc.new do   
  puts “<3 Jonan”
end