Блоки — одна из самых мощных и часто игнорируемых фич руби. Признаюсь, у меня ушло прилично времени чтобы разобраться как работают блоки и насколько они могут быть полезными на практике.
Есть нечто в 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_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). В контексте метода 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 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 как о способе вводить этот код в произвольное место в методе. Это значит что у вас может быть один метод который работает по разному, теперь вам не нужно множество функций (вы можете переиспользовать одну единственную для множества разных вещей). Вы справились! Прочтя этот пост до конца, вы встали на путь поиска способов оригинального использования руби блоков. Если по какой-то причине вы всё ещё ощущаете растерянность, прошу рассказать в комментариях о чём следует рассказать подробнее. И поделитесь этой статьёй если узнали что-то новое о руби блоках.
ссылка на оригинал статьи https://habr.com/ru/post/703920/
Добавить комментарий