Ruby и .

Блоки в Ruby

Послемыслия и работа над ошибками

  • добавлен раздел о poetry mode (01.08.11)

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

Когда возникает необходимость в блоках?

Представим, вы создали простой класс, описывающий гладиатора:

# gladiator.rb

class Gladiator
  attr :name

  def initialize(name)
    @name = name
  end

  def say_hello
    puts "Поклон тебе, Цезарь!"
    puts "Идущие на смерть приветствуют тебя!"
  end

  # остальное опустим для краткости
  # ...
end

Пример использования:

:001 > require "./gladiator"
:002 > spartak = Gladiator.new("Спартак")
:003 > spartak.say_hello
Поклон тебе, Цезарь!
Идущие на смерть приветствуют тебя!
 => nil

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

def say_hello
  puts "Поклон тебе, Цезарь!"

  # процедура составления завещания
  # ...

  puts "Идущие на смерть приветствуют тебя!"
end

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

Пока вы думаете над проблемой, приходит еще одно письмо, на этот раз от программистов из самого Pixar! По странному стечению обстоятельств им понадобилось такое:

def say_hello
  puts "Поклон тебе, Цезарь!"

  # запустить анимацию наклона персонажа вперед
  # ...

  puts "Идущие на смерть приветствуют тебя!"
end

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

Как раз для этого в Ruby используют блоки.

Что такое блок?

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

  • методу можно передать только один блок;
  • он всегда идет в самом конце и вынесен за скобки с аргументами (если они есть);
  • его можно задать при вызове любого метода, независимо от того, указаны в определении метода аргументы или нет.

Код может находиться внутри фигурных скобок { } или ключевых слов do end. Что именно использовать — решать вам, потому что обе конструкции описывают совершенно одинаковые по свойствам блоки. Обычно в фигурные скобки заключают код, состоящий из одной строки, при этом метод и его блок записывают на одной строке. Если код блока слишком длинный или объемный, вместо скобок на строке с вызовом метода оставляют do, а сам код пишут с новой строки с отступом и в конце «закрывают» его end'ом.

Посмотрим, как это выглядит на практике:

:004 > spartak.say_hello { puts "Отпустите меня домой" }
Поклон тебе, Цезарь!
Идущие на смерть приветствуют тебя!
 => nil

Как видим, Ruby совершенно не возражает, что мы добавили к методу блок, хотя сам метод ничего о нем не знает! При этом метод выполнился без изменений. Что, если записать выражение иначе?

:005 > spartak.say_hello do
:006 >   puts "Отпустите меня домой"
:007?> end
Поклон тебе, Цезарь!
Идущие на смерть приветствуют тебя!
 => nil

Абсолютно никакой разницы. Но какой толк от блока, который не выполняется?

Передача контроля блоку

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

yield — это часть синтаксиса языка, как и объявление блока (поэтому ее нельзя переопределить). Когда в теле метода мы вызываем yield, то подразумеваем следующее: «отдай управление блоку, а когда тот закончит, верни управление этому методу».

Попробуйте переписать приветствующий метод так:

class Gladiator
  def say_hello
    puts "Поклон тебе, Цезарь!"
    yield
    puts "Идущие на смерть приветствуют тебя!"
  end
end

и еще одна попытка передать блок:

:003 > spartak.say_hello { puts "То, что мы делаем в жизни, отзывается в вечности." }
Поклон тебе, Цезарь!
То, что мы делаем в жизни, отзывается в вечности.
Идущие на смерть приветствуют тебя!
 => nil

Вуаля! Теперь все надоедливые товарищи могут пихать в этот блок, что захочется, и не мешать вам жить. Ура? Не совсем: попробуйте вызвать метод без блока.

:004 > spartak.say_hello
LocalJumpError: no block given (yield)

Ошибка! Ruby теперь в обязательном порядке требует от нас блок! В некоторых случаях без блока действительно никак, но чаще всего логика метода вполне позволяет обойтись без него, как в этом случае. Поэтому перед вызовом беспощадного yield следует удостовериться, а есть ли блок, с помощью метода block_given?:

def say_hello
  puts "Поклон тебе, Цезарь!"
  yield if block_given?
  puts "Идущие на смерть приветствуют тебя!"
end

:003 > spartak.say_hello
Поклон тебе, Цезарь!
Идущие на смерть приветствуют тебя!
 => nil

Обмен информацией с блоком

Только вы расслабились, как ребята из страховой компании стучатся с новой проблемой. Им нужно вписать в завещание имя гладиатора, но решение «в лоб» не работает:

:003 > spartak.say_hello do
:004 >   puts "ЗАВЕЩАНИЕ"
:005?>   puts "Я, #{name}, настоящим завещаю"
:006?> end
Поклон тебе, Цезарь!
ЗАВЕЩАНИЕ
NameError: undefined local variable or method 'name' for main:Object

Почему так произошло, несмотря на то, что метод name у класса Gladiator определен? Дело в том, что хотя блок и вызывается непосредственно в методе, ему недоступны ни локальные переменные этого метода (say_hello), ни атрибуты объекта (spartak), метод которого вызывается.

Эту проблему можно решить несколькими способами. Первый — явно передать блоку ту информацию, в которой он может нуждаться. Например, предоставить ему имя гладиатора:

def say_hello
  puts "Поклон тебе, Цезарь!"
  yield(name) if block_given?
  puts "Идущие на смерть приветствуют тебя!"
end

Зная об этом, страховщики теперь могут писать:

:003 > spartak.say_hello do |gladiator_name|
:004 >   puts "ЗАВЕЩАНИЕ"
:005?>   puts "Я, #{gladiator_name}, настоящим завещаю"
:006?> end
Поклон тебе, Цезарь!
ЗАВЕЩАНИЕ
Я, Спартак, настоящим завещаю
Идущие на смерть приветствуют тебя!
 => nil

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

У метода есть имя. Назвали имя — вызвали код, описанный в определении метода. Назвали yield — вызвали код, описанный в блоке.

Метод может принимать аргументы, если они были указаны в его определении. Как видно из последнего примера, yield тоже можно вызывать с аргументами. И было бы логичным ожидать, что в самом блоке нужно как-то указать перечень принимаемых аргументов.

Ожидания оправдываются во всём, кроме синтаксиса: вместо круглых скобок, как в методе, аргументы блока следует брать в прямые | |. После того, как аргумент объявлен, можно обращаться к нему в блоке, как к локальной переменной. Список аргументов записывается сразу после do (или {), перенос строки после него не обязателен.

Продолжим аналогию. Метод всегда возвращает значение: это или значение последнего выражения, или значение выражения после ключевого слова return. Блок тоже возвращает значение, и его можно получить в методе как результат вызова yield.

Пусть наш салютующий метод теперь выглядит так:

def say_hello
  puts "Поклон тебе, Цезарь!"
  if block_given?
    # монетка бросается в блоке
    coin_flip = yield(name)
    if coin_flip == 1
      # выпал аверс
      puts "Похоже, мне повезет в этой битве."
    else
      puts "Смерть улыбается всем нам."
    end
  end
  puts "Идущие на смерть приветствуют тебя!"
end

а его вызов с блоком — так:

:003 > spartak.say_hello do |gladiator_name|
:004 >   puts "-> #{gladiator_name} бросает монетку"
:005?>   rand(2)
:006?> end
Поклон тебе, Цезарь!
-> Спартак бросает монетку
Похоже, мне повезет в этой битве.
Идущие на смерть приветствуют тебя!
 => nil

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

:001 > def nonsense
:002?>   inside_method = "Я существую только внутри метода."
:003?>   yield if block_given?
:004?> end
 => nil
:005 > nonsense
 => nil
:006 > puts inside_method
NameError: undefined local variable or method 'inside_method' for main:Object
:007 > nonsense { inside_block = "А я — только внутри блока." }
 => "А я — только внутри блока."
:008 > puts inside_block
NameError: undefined local variable or method 'inside_block' for main:Object

Обратите внимание на то, как значение присвоения переменной inside_block было последовательно возвращено блоком в метод, а затем — самим методом. Впрочем, это всё равно не сделало саму переменную видимой вне блока.

Почему блок нельзя считать анонимным методом?

Внешние переменные

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

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

:001 > cat = "Голодный кот"
 => "Голодный кот"
:002 > def locate_cat
:003?>   puts "Кажется, #{cat} на крыше."
:004?> end
 => nil
:005 > locate_cat
NameError: undefined local variable or method 'cat' for main:Object
:006 > def whatever
:007?>   yield
:008?> end
 => nil
:009 > whatever do
:010 >   puts "#{cat} добрался до колбасы!"
:011?>   cat = "Уже не голодный кот"
:012?> end
Голодный кот добрался до колбасы!
 => "Уже не голодный кот"
:013 > puts cat
Уже не голодный кот
 => nil

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

:014 > whatever { dog = "Ленивый пёс" }
 => "Ленивый пёс"
:015 > whatever { puts dog }
NameError: undefined local variable or method 'dog' for main:Object

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

:003 > spartak.say_hello do
:004 >   puts "ЗАВЕЩАНИЕ"
:005?>   puts "Я, #{spartak.name}, настоящим завещаю"
:006?> end

Столкновение переменных

Что произойдет, если имя аргумента блока совпадает с внешней переменной? В этом случае Ruby создаст новую локальную переменную, и внешняя переменная внутри блока будет недоступна.

:001 > def with_one
:002?>   yield(1)
:003?> end
:004 > number = 99
:005 > with_one { |number| puts "number равно #{number}" }
number равно 1
 => nil
:006 > puts number
99
 => nil

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

:006 > puts number
99
 => nil
:007 > with_one do |i; number|
:008 >   puts "i равно #{i}"
:009?>   number = 123
:010?> end
i равно 1
 => 123
:011 > puts number
99
 => nil

Как это использовать?

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

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

Как метод и блок считают аргументы

В этом еще одно отличие: метод слишком ворчливый и дотошный, он требует строгого соответствия заявленного и предоставленного количества аргументов. Блок же закрывает глаза на недостачу и даже на избыток аргументов!

:001 > def feed_crocodile(name)
:002?>   puts "Сейчас будем кормить крокодила #{name}"
:003?>   yield(name) if block_given?
:004?> end
 => nil
:005 > feed_crocodile("Тотошку")
Сейчас будем кормить крокодила Тотошку
 => nil
:006 > feed_crocodile
ArgumentError: wrong number of arguments (0 for 1)
:007 > feed_crocodile("Тотошку", "бананами")
ArgumentError: wrong number of arguments (2 for 1)
:008 > feed_crocodile("Тотошку") { |name| puts "#{name} кормят мясом!" }
Сейчас будем кормить крокодила Тотошку
Тотошку кормят мясом!
 => nil
:009 > feed_crocodile("Тотошку") { puts "Два дня не ел, бедняга" }
Сейчас будем кормить крокодила Тотошку
Два дня не ел, бедняга
 => nil
:010 > feed_crocodile("Тотошку") { |name, food| puts "#{name} кормят #{food}!" }
Сейчас будем кормить крокодила Тотошку
Тотошку кормят !
 => nil

Нечто, похожее на блоки

Существует ряд операторов в Ruby, в которых используются ключевые слова do end — это циклы while, until, for. Хочется предостеречь от возможного заблуждения: в них не используются блоки, do end являются неотъемлемой составляющей конструкции этих операторов.

Во-первых, хотя и пишем

:001 > for i in [1, 2, 3] do
:002 >   print i
:003?> end
123 => [1, 2, 3]

но не получится написать

for i in [1, 2, 3] { print i }

Во-вторых, операторы циклов не создают локальную область видимости:

:001 > x = 0
 => 0
:002 > while x < 1 do
:003 >   inside = "Я внутри?"
:004?>   x += 1
:005?> end
 => nil
:006 > puts inside
Я внутри?
 => nil

В то же время loop, реализующий бесконечный цикл, является системным методом, который как раз принимает блоки:

loop { print "кольцокольцо" }

Поэтичный стиль и блоки

Поэтичным стилем (poetry mode) в Ruby называют стиль написания кода, при котором опускают скобки в тех местах, где анализатор может предположить их наличие, исходя из контекста. В основном это касается аргументов при вызове метода:

puts("Hello") # классический стиль
puts "Hello"  # poetry mode

Разница между { } и do end

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

Допустим, есть метод kick_ass, который принимает в качестве единственного аргумента имя того, кому мы хотим надрать задницу. Тогда запись

kick_ass "Джокер" do
  puts "Ты будешь сидеть в камере психушки вечно."
end

будет прочитана Ruby именно так, как вы задумали:

kick_ass("Джокер") do
  puts "Ты будешь сидеть в камере психушки вечно."
end

А вот вызов метода с блоком в фигурных скобках

kick_ass "Бэтмен" { puts "Если умеешь что-то, не делай этого бесплатно." }

Ruby поймет — внимание! — как:

kick_ass("Бэтмен" { puts "Если умеешь что-то, не делай этого бесплатно." })

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

Блок и хеш

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

kick_ass { :joker => "2 раза", :penguin => "1 раз" } # синтаксическая ошибка
kick_ass :joker => "2 раза", :penguin => "1 раз"     # так всё нормально

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

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

Итераторы

Трудно себе представить рабочий код на Ruby без применения итераторов. Вся работа с массивами, хешами и другими объектами, способными себя перечислять, построена на итераторах, что намного удобнее использования циклов.

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

Самый известный итератор, each, последовательно передает в блок все элементы объекта, от имени которого вызван метод.

:001 > [1, 2, 3].each { |i| puts i + 2 }
3
4
5
 => nil

Обёртывание кода

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

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

Классическим примером является работа с файлами с помощью системного класса File. Метод open этого класса берет на себя заботу о создании дескриптора открытого файла и — главное — корректном закрытии файла в конце, пользователю же остается только самое важное — считывание и обработка данных, находящихся в файле.

:001 > first_line = File.open("some.txt") do |file|
:002 >   file.readline
:003?> end

Бывает и обратная ситуация: когда вы хотите выполнить что-то перед или после постороннего метода. Для этого создается служебный метод, в котором будет выполняться это «что-то» и происходить вызов блока. Теперь достаточно передать посторонний метод в блоке вашему служебному методу.

Классическим примером является измерение времени выполнения метода:

def measure_time
  # запустить таймер
  # ...

  yield

  # остановить таймер и вывести потраченное время
  # ...
end

measure_time { make_coffee }

DSL

Если вы едите бутерброды каждый день, вполне возможно, вам захочется написать для этого целую библиотеку. В ней будет много-много методов, вроде «намазать», «нарезать» и «открыть», каждый из которых будет представлять собой низкоуровневый код на обычном Ruby. С этим кодом вполне можно работать и напрямую, но чтобы разобраться в алгоритме работы в контексте поставленной задачи, нужно напрячься. Другое дело — такая вот инструкция, всё строго по делу:

slices = cut bread { by 3, :slices }
open caviar_can, :with => :can_opener do |contents|
  slices.each { |slice| slice.spread :with => contents.about('20%') }
end

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

Классическим примером DSL является фреймворк для тестирования RSpec.

Конструктор объекта

Иногда конструктор объекта должен принять столько различных параметров, что передача их через список аргументов уже неудобна: легко запутаться, что чему мы присваиваем. Одним из вариантов решения этой проблемы является использование хеша {название параметра => значение параметра, …}.

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

class Gladiator
  attr_accessor :name, :rank, :height, :weight

  def initialize
    yield(self)
  end

  #...
end

spartak = Gladiator.new do |his|
  his.name = "Спартак"
  his.weight = 91
end