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

Есть нечто в 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 ссылается на блок и может быть выполнена с помощью метода callcall — тоже что и 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_app(array)
  new_array = []
  
  for element in array
    new_array.push yield element
  end
  
  new_array
end

my_map([1,2,3]) do |number|
  number * 2
end
2
4
6

Инициализация объектов с дефектными значениями

Классный шаблон, который можно использовать с руби блоками — инициализации объекта со значениями по умолчанию. Вы возможно уже видели этот этот шаблон, если хоть раз открывали .gemspec любого гема. Это работает так, у вас есть инициализатор который вызывает yield(self). В контексте метода initializeself это объект который инициализируется.

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 dp |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 как о способе вводить этот код в произвольное место в методе. Это значит что у вас может быть один метод который работает по разному, теперь вам не нужно множество функций (вы можете переиспользовать одну единственную для множества разных вещей). Вы справились! Прочтя этот пост до конца, вы встали на путь поиска способов оригинального использования руби блоков. Если по какой-то причине вы всё ещё ощущаете растерянность, прошу рассказать в комментариях о чём следует рассказать подробнее. И поделитесь этой статьёй если узнали что-то новое о руби блоках.

Комментарии (0)