Блоки в 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