Блоки — одна из самых мощных и часто игнорируемых фич руби. Признаюсь, у меня ушло прилично времени чтобы разобраться как работают блоки и насколько они могут быть полезными на практике.
Есть нечто в yield
, что делает его крайне трудным для понимания при первом рассмотрении. Я хочу поговорить о концептах и приведу несколько примеров, так что к концу этого поста у вас появится твёрдое понимание руби блоков.
Оригинал этого поста — Mastering ruby blocks in less than 5 minutes — был опубликован в блоге «Mix & Go» 20 января 2015, автор: Цезарь Хелмеджин.
Основы: Что такое руби блок?
Блок это просто код который вы ставите между do
и end
. Вот и всё. «Но где же магия?», — спросите вы. Мы доберёмся до магии через минуту, для начала разберёмся с основами. Блок можно записать двумя способами: (1) многострочный, между do
и end
, и (2) однострочный, между {
и }
. Обе версии делают абсолютно одно и тоже, так что только вам решать какой вариант использовать. Очевидно, что однострочное написание следует использовать когда метод занимает одну строку, а многострочного когда много.
Базовый пример многострочного блока:
[1, 2, 3,].each do |n|
puts "Number #{n}"
end
Это называется многострочным блоком, потому что записывается в несколько строк, а не потому что сам состоит из множества строк кода (что видно на примере выше). Тот же пример может быть записан в одну строку:
[1, 2, 3].each { |n| puts "Number #{n}" }
Обе версии выведут числа 1, 2 и 3. Буква n, которую вы можете наблюдать между пайпами (|n|
), называется параметр блока и его значением будет каждая цифра по очереди, в том порядке в котором они идут в массиве. Так, на первой итерации, значением n будет 1, на второй соответственно 2, и 3 на третьей.
Number 1
Number 2
Number 3
=> [1, 2, 3]
Как работает yield
yield
в ответе за всю неразбериху и магию вокруг руби блоков. Я думаю смятение вызывает то как yield
вызывает блок и как передаёт ему параметр. Мы рассмотрим оба сценария в этой части.
def my_method
puts "reached the top"
yield
puts "reached the bottom"
end
my_method do
puts "reached yield"
end
reached the top
reached yield
reached the bottom
=> nil
Когда выполнение my_method
достигает строчки где вызывается yield
, выполняется код из переданного блока. После, когда выполнение кода из блока заканчивается, выполнение my_method
продолжается.
Выполнение руби блока
Передача блока методу
Чтобы метод мог принимать блок в качестве параметра, это НЕ нужно явно указывать в определении метода. Вы можете передать блок любой функции, однако если функция не вызывает yield
, блок не будет выполнен. В тоже время, если вы вызываете yield
в теле метода, использование блока в качестве аргумента становится обязательным, исполнение метода приведёт к исключению (exception) если он не получит блок на вход. Если вы всё же хотите использовать блок, но в качестве опционального параметра, вы можете воспользоваться методом block_given?
который вернёт true
или false
в зависимости от того передан блок в качестве аргумента или нет. yield
тоже принимает параметры Любой аргумент переданный yield
будет использован как аргумент блока. Так что, когда блок выполняется, он может использовать параметры переданные начальному методу. Эти параметры могут быть локальными переменными метода, того в котором вызывается yield
. Порядок аргументов очень важен, потому что блок получит аргументы именно в таком порядке в котором вы их определили.
Примечательно то, что параметры внутри блока локальны самому блоку (в отличии от тех что передаются из метода в блок).
Что такое &block (амперсанд аргумент)?
Вы наверняка уже видели этот &block
в каком-нибудь примере руби кода. Это то как вы можете передать указатель на блок (вместо локальной переменной) в качестве параметра функции. Руби позволяет передать любой объект методу как если бы этот объект был блоком. Метод попытается использовать объект так если бы он был блоком, однако если это не блок, то на объекте будет вызван to_proc
в попытке конвертировать его в блок. Также обратите внимание что block
(без амперсанда) это всего лишь имя указателя, вы можете использовать любое слово вместо него.
def my_method(&block)
puts block
block.call
end
my_method { puts "Hello" }
#<Proc:0x0000010124e5a8@tmp/example.rb:6>
Hello!
Как вы можете наблюдать выше, переменная block
внутри my_method
ссылается на блок и может быть выполнена с помощью метода call
. call
— тоже что и yield
, некоторые рубисты предпочитают использовать block.call
вместо yield
, по причинам читабельности.
Возврат значения
yield
возвращает последнее рассчитанное выражение (изнутри блока). Иными словам, значение возвещаемое yield
это значение которое возвращает блок.
def my_method
value = yield
puts "value is: #{value}"
end
my_method do
2
end
value is 2
=> nil
Как работает .map(&:something)?
Вероятно вы уже пользовались шорткатами вроде .map(&:capitalize)
достаточно много, особенно если занимались кодом рельс. Это вполне понятное сокращение от .map { |title| title.capitalize }
.
Как оно работает в действительности?
Оказывается класс Symbol
имплементирует метод to_proc
который разворачивает сокращение до полной версии. Круто, да?
Как построить собственный итератор
Вы можете вызывать yield
внутри метода столько раз сколько захотите. В принципе это то как работает итератор. Вызов yield
для каждого элемента массива имитирует поведение нативных итераторов руби.
Рассмотрим метод похожий на стандартный руби метод map
.
def my_map(array)
new_array = []
for element in array
new_array.push yield element
end
new_array
end
my_map([1,2,3]) do |number|
puts number * 2
end
2
4
6
Инициализация объектов с дефектными значениями
Классный шаблон, который можно использовать с руби блоками — инициализации объекта со значениями по умолчанию. Вы возможно уже видели этот этот шаблон, если хоть раз открывали .gemspec
любого гема. Это работает так, у вас есть инициализатор который вызывает yield(self)
. В контексте метода initialize
, self
это объект который инициализируется.
class Car
attr_accessor :color, :doors
def initialize
yield(self)
end
end
car = Car.new do |c|
c.color = "Red"
c.doors = 4
end
puts "My car's color is #{car.color} and it's got #{car.doors} doors."
My car's colour is Red and it's got 4 doors.
Примеры руби блоков
Примеры нынче в моде, так что давайте поищи интересные способы использования блоков в реальном мире (или как можно ближе к нему).
Обертывание текста html тегами
Блоки это идеальный кандидат в тех случаях когда вам нужно обернуть кусок динамического кода каким-нибудь статическим кодом. Например если вы хотите генерить html теги для текста. Текст это динамическая часть (потому что заранее не известно что нужно обернуть), теги — статическая, они не меняются.
def wrap_in_h1
"<h1>#{yield}</h1>"
end
wrap_in_h1 { "Here's my heading" }
# => "<h1>Here's my heading</h1>"
wrap_in_h1 { "Ha" * 3 }
# => "<h1>HaHaHa</h1>"
Преимущества использования блоков, в сравнении с методами, очевидны когда вам нужно переиспользовать некоторое поведение с небольшими изменениями в нём.
def wrap_in_tags(tag, text)
html = "<#{tag}>#{text}</#{tag}>"
yield html
end
wrap_in_tags("title", "Hello") { |html| Mailer.send(html) }
wrap_in_tags("title", "Hello") { |html| Page.create(:body => html) }
В первом случае мы отправляем <title>Hello</title>
по электронной почте, а во втором создаём запись Page. В обоих случаях это один и тот же метод выполняющий разные задачи. На заметку Допустим нам нужен быстрый способ записывать свои идеи в таблицу базы данных. Для этой задачи нам нужно передавать текст заметки и как-то подключаться к базе данных. В идеале мы хотим вызывать Note.create { “Nice day today” }
и не беспокоиться об открытии и закрытии подключения к базе данных. Так что поступим следующим образом:
class Note
attr_accessor :note
def initialize(note=nil)
@notne = note
puts "@note is #{@note}"
end
def self.create
self.connect
note = new(yield)
note.write
self.disconnect
end
def write
puts "Writing \"#{@note}\" to the database."
end
private
def self.connect
puts "Connecting to the database..."
end
def self.disconnect
puts "Disconnecting from the database..."
end
end
Note.create { "Foo" }
Connecting to the database...
@note is Foo
Writing "Foo" to the database.
Disconnecting from the database...
Поиск кратных элементов массива
Похоже я удаляюсь от “реального мира” всё дальше и дальше, в любом случае, я хочу привести последний пример. Допустим вам нужен каждый элемент массива кратный 3 (или любому другому числу на выбор), что насчёт руби блоков?
class Fixnum
def to_proc
Proc.new do |obj, *args|
obj % self == 0
end
end
end
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].select(&3)
puts numbers
3
6
9
Заключение
Вы можете думать о блоках просто как о кусках кода, а yield
как о способе вводить этот код в произвольное место в методе. Это значит что у вас может быть один метод который работает по разному, теперь вам не нужно множество функций (вы можете переиспользовать одну единственную для множества разных вещей). Вы справились! Прочтя этот пост до конца, вы встали на путь поиска способов оригинального использования руби блоков. Если по какой-то причине вы всё ещё ощущаете растерянность, прошу рассказать в комментариях о чём следует рассказать подробнее. И поделитесь этой статьёй если узнали что-то новое о руби блоках.