Ruby и .

Проки и лямбды

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

  • добавлены раздел «Метод как объект» и сводная таблица (16.08.11)
  • упрощено описание хуков в Rails (16.08.11)

Как только ваш вестибулярный аппарат освоился с прыжками между методом и блоком при участии yield, пора переходить к более серьезным вещам.

В этой статье рассказывается о том, как можно хранить блоки и зачем это делают.

Не yield'ом единым

Возможно, в далеких неизведанных галактиках существует планета, в которой отдать управление блоку в Ruby можно только с помощью ключевого слова yield. Разработчики в этом мире очень несчастны. Каждый раз, когда они передают блок методу, его тут же надо где-то в методе и оприходовать. Всё в ужасной спешке, знаете ли. А представляете, что произойдет, если забыл написать yield? Метод вызван, а блок его пропал, исчез в черной пучине цифрового ничто!

Но может, зря они там нервничают? Ведь в Ruby полно примеров, когда блок используется тут же, прямо в методе, которому он передается. Взять, например, популярный итератор map:

 :001 > wtf = %w" ) ( -> ] "
  => [")", "(", "->", "]"]
 :002 > smiles = wtf.map { |el| ':' + el }
  => [":)", ":(", ":->", ":]"]

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

Но так бывает не всегда. И чтобы это прочувствовать, мы напишем маленький стартап.

Сервис «Который час?»

Наш стартап гениален в своей простоте. Когда пользователь заходит по адресу http://который.час/название-города, мы показываем ему точное местное время. Отвечаем обычной строкой, без html разметки, поэтому затраты на трафик будут минимальны, что очень хорошо для мобильных пользователей.

Для написания веб-приложения нам не понадобится особых знаний протокола HTTP, потому что всю заботу по обработке запросов возьмет на себя микрофреймворк Sinatra. Собственно говоря, приложение на Sinatr'е — это DSL, который описывает, на какие запросы стоит отвечать серверу и что при этом нужно делать.

Начнем с простого, напишем обработку запроса http://который.час/london

# encoding: utf-8
require 'sinatra'

get '/london' do
  london_time = Time.now.utc
  "В Лондоне сейчас: #{london_time}"
end

Вы не поверите, но это всё наше приложение. Да, оно не учитывает переход на летнее время (BST), поэтому с конца мая до октября, когда Британия переводит стрелки на час вперед от времени по Гринвичу (UTC+1), мы будем показывать неверное время, но я не буду в этом примере загромождать код.

Sinatra должен понять этот так: когда я получаю HTTP GET запрос от клиента (читай — браузера), URL которого совпадает с "/london" (доменное имя отбрасывается), то должен выполнить приведенный блок, и его результат (строку) вернуть клиенту в теле ответа.

Сохраним написанное в файл what_time.rb и запустим приложение (для начала локально):

$ ruby what_time.rb
== Sinatra/1.2.6 has taken the stage on 4567 for development with backup from WEBrick

По умолчанию Sinatra занимает порт 4567, туда и обратимся с запросом из браузера: http://0.0.0.0:4567/london, чтобы увидеть в его окне желанный ответ:

В Лондоне сейчас: 2011-08-09 14:11:21 UTC

А теперь поставьте себя на место интерпретатора Ruby и внимательно проследите за хронологией.

what_time.rb хранит в себе обычное приложение, в котором есть вызов метода get, что принимает в качестве аргумента строку, плюс мы передаем ему блок. Еще раз: это не определение метода, а его вызов, который происходит в момент запуска сервера, когда Ruby считывает и выполняет what_time.rb.

Это означает, что когда мы посылаем свой запрос из браузера, метод get вместе с переданным ему блоком уже давным-давно выполнен. И в то же время Ruby нужно отдать контроль блоку именно в момент запроса.

В этом месте жители планеты «Только yield» заливаются горючими слезами. А мы переходим к следующему разделу.

Процедурные объекты

Интуитивно мы понимаем, что где-то внутри метода get хитрые разработчики Sinatra сохраняют блок про запас, чтобы выполнить его при получении соответствующего запроса от клиента. Но как это сделать?

Все данные в Ruby — это объект. Числа, строки, массивы, хеши, классы, экземпляры классов и даже великий nil — всё это объекты, у каждого из которых есть свой набор методов, и которые можно хранить в переменных.

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

 :001 > say_hello = do
 :002 >   word = "Привет!"
 :003?>   puts word
 :004?> end
  SyntaxError: (irb):1: syntax error, unexpected keyword_do_block
  (irb):4: syntax error, unexpected keyword_end, expecting $end

Чтобы превратить блок в объект, в Ruby существует несколько способов.

Самый явный — это получение процедурного объекта при помощи системного метода lambda или метода new класса Proc. Оба они принимают блок и создают объект класса Proc:

 :001 > say_hello = lambda do
 :002 >   word = "Привет!"
 :003?>   puts word
 :004?> end
  => #<Proc:0x8c56ff4@(irb):1 (lambda)>
 :005 > say_hello.class
  => Proc
 :006 > how_do_you_do = Proc.new { puts "Как дела?" }
  => #<Proc:0x8e40090@(irb):6>
 :007 > how_do_you_do.class
  => Proc

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

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

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

Красной кнопкой служит метод call, выполняющий тело процедурного объекта.

 :008 > say_hello.call
 Привет!
  => nil
 :009 > how_do_you_do.call
 Как дела?
  => nil

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

:001 > japanese_smile = lambda do |left_eye, right_eye = left_eye|
:002 >   "(#{left_eye}.#{right_eye})"
:003?> end
 => #<Proc:0x9f93284@(irb):1 (lambda)>
:004 > japanese_smile.call('^')
 => "(^.^)"
:005 > japanese_smile.call '-'
 => "(-.-)"
:006 > japanese_smile.call '>', '<'
 => "(>.<)"

У call есть более короткая форма записи:

:007 > japanese_smile.('о', 'O')
 => "(о.O)"

Лямбда против прока

Хотя все процедурные объекты в Ruby одного класса (Proc), те, что созданы с помощью lambda, отличаются от созданных методом Proc.new. Первые называют (по имени метода-родителя) лямбдами (lambda), вторые — проками (proc).

Формальные признаки

Вы могли уже заметить, что строковые представления лямбды и прока отличаются: Ruby добавляет к первой уточнение в скобках. Но если в коде нужно проверить, с чем имеете дело, удобнее использовать специальный метод-детектор — lambda?

:001 > lambda {}.to_s
 => "#<Proc:0x97f755c@(irb):1 (lambda)>"
:002 > Proc.new {}.to_s
 => "#<Proc:0x97db398@(irb):2>"
:003 > lambda {}.lambda?
 => true
:004 > Proc.new {}.lambda?
 => false

Аргументы

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

Так вот, прок совершенно прозрачно повторяет логику блока, в то время как лямбда добавляет к переданному ей блоку педантичности метода.

:001 > link_to = lambda { |href, text| "<a href='#{href}'>#{text}</a>" }
 => #<Proc:0x9abb89c@(irb):1 (lambda)>
:002 > link_to.call 'http://который.час', 'Который час?'
 => "<a href='http://который.час'>Который час?</a>"
:003 > link_to.call 'http://который.час'
ArgumentError: wrong number of arguments (1 for 2)
:004 > link_to.call 'http://который.час', 'Который час?', 'лишнее'
ArgumentError: wrong number of arguments (3 for 2)
:005 > link_to = Proc.new { |href, text| "<a href='#{href}'>#{text}</a>" }
 => #<Proc:0x9bfcaa8@(irb):5>
:006 > link_to.call
 => "<a href=''></a>"
:007 > link_to.call 'http://который.час', 'Который час?', 'лишнее'
 => "<a href='http://который.час'>Который час?</a>"

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

return

Ключевое слово return в блоке лямбды прерывает его выполнение и возвращает управление в то окружение (метод или блок), где была вызвана лямбда.

С проком сложнее: return вначале перемещает нас в то окружение, где был определен его блок, а затем выполняет return из него.

Пусть у нас есть такой метод:

# casino.rb

def enter_casino(entrance_fee, bouncer_type)
  puts "Вы подходите к казино и даете #{entrance_fee}$"

  bouncer = case bouncer_type
  when "маленький вышибала"
    lambda do |entrance_fee|
      return "«Маловато денег даешь, парниша»" if entrance_fee < 50
      "«Проходите, добрый сэр!»"
    end
  when "большой вышибала"
    Proc.new do |entrance_fee|
      return "«Чо за дела?!»" if entrance_fee < 50
      "«Добро пожаловать и приятного вечера!»"
    end
  end

  puts bouncer.call(entrance_fee)
  puts "Вы зашли внутрь."
end

Проверим сначала лямбду:

:001 > require './casino'
 => true
:002 > enter_casino 50, "маленький вышибала"
Вы подходите к казино и даете 50$
«Проходите, добрый сэр!»
Вы зашли внутрь.
 => nil

Без неожиданностей. Теперь попробуем обмануть нашего вышибалу:

:003 > enter_casino 5, "маленький вышибала"
Вы подходите к казино и даете 5$
«Маловато денег даешь, парниша»
Вы зашли внутрь.
 => nil

Отлично! return не дал добраться до конца блока, поэтому лямбда вернула «Маловато…» вместо «Проходите…». Но на выполнение метода enter_casino это никак не повлияло, он продолжил работу ровно с того момента, как передал управление лямбде, т. е. с предпоследнего метода puts.

Поставим теперь на входе прок:

:004 > enter_casino 5, "большой вышибала"
Вы подходите к казино и даете 5$
 => «Чо за дела?!»

Бум! return произошел не только из тела прока, но и из enter_casino. Методу даже не дали выполнить puts, чтобы напечатать «Чо за дела?!» (эти слова вернул сам метод после аварийного return).

Тот факт, что Ruby пытается выполнить return из окружения (метода), где был определен блок прока, а не из того, где он вызван (в примере с казино эти окружения совпали), может приводить к ошибкам LocalJumpError.

:001 > outer_proc = Proc.new { return }
 => #<Proc:0x8dfc098@(irb):1>
:002 > def call_proc(pr)
:003?>   pr.call
:004?>   puts "Этой строки мы не увидим"
:005?> end
 => nil
:006 > call_proc(outer_proc)
LocalJumpError: unexpected return
:001 > def make_proc
:002?>   Proc.new { return }
:003?> end
 => nil
:004 > def call_proc
:005?>   make_proc.call
:006?> end
 => nil
:007 > call_proc
LocalJumpError: unexpected return

Это подтверждает то, что блок, несмотря на превращение в процедурный объект, остается и выполняется на том же месте в коде приложения, где вы его написали.

Мнемонику для return можно использовать такую: лямбда — существо маленькое (пишется со строчной буквы), поэтому силы его слова хватает только на себя само, прок — большая шишка (Proc.new, с прописной), его return'а хватает и на окружающий метод или блок.

Достать блок

Замороженный yield

Вернемся к нашим попыткам понять, как устроен Sinatr'овский метод get:

get '/london' do
  london_time = Time.now.utc
  "В Лондоне сейчас: #{london_time}"
end

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

# fake_sinatra.rb

class FakeSinatra
  def initialize
    @blocks = {}
  end

  def exec(url)
    block = @blocks[url]
    block.call if block
  end

  def get(url)
    @blocks[url] = # как?
  end
end

До сих пор с переданным методу блоком мы связывались с помощью ключевого слова yield. Но если написать так

def get(url)
  @blocks[url] = yield
end

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

def get(url)
  @blocks[url] = Proc.new { yield }
end

Хм-м-м. Опять же непонятно, будет ли yield вызываться каждый раз при обращении к проку или в теле прока осядет статический результат выполнения блока? Давайте проверим:

:001 > require './fake_sinatra'
 => true
:002 > sinatra_app = FakeSinatra.new
 => #<FakeSinatra:0x9b6f504 @blocks={}>
:003 > sinatra_app.get '/london' do
:004 >   london_time = Time.now.utc
:005?>   "В Лондоне сейчас: #{london_time}"
:006?> end
 => #<Proc:0x9abb518@(irb):3>
:007 > sinatra_app
 => #<FakeSinatra:0x9b6f504 @blocks={"/london"=>#<Proc:0x9abb518@(irb):3>}>
:008 > sinatra_app.exec '/london'
 => "В Лондоне сейчас: 2011-08-11 08:01:42 UTC"
:009 > sinatra_app.exec '/london'
 => "В Лондоне сейчас: 2011-08-11 08:01:45 UTC"

Что ж, подход оказался вполне рабочим. Несмотря на то, что фактически мы реализовали отложенное обращение к yield через прок, это дало нужный результат.

Но необходимость двойного вызова (вначале call, потом yield) для того, чтобы добраться до блока, оставляет ощущение, что это не самый простой и правильный способ. К счастью, в Ruby предусмотрено несколько способов создания процедурного объекта напрямую из переданного в метод блока.

Proc.new

Если внутри метода вызвать Proc.new без блока, Ruby воспримет это как указание создать прок из внешнего блока.

:001 > def act_as_yield
:002?>   Proc.new.call if block_given?
:003?> end
 => nil
:004 > act_as_yield { 2 + 3 }
 => 5

Поэтому метод get для FakeSinatra может быть переписан так:

def get(url)
  @blocks[url] = Proc.new
end

Псевдоаргумент

Для Ruby блок — особый персонаж, его можно передавать любому методу, ничего не указывая в списке аргументов. Но всякое тайное рано или поздно становится явным. Так и наш псевдоаргумент-блок — его, оказывается, можно вписывать в общий список!

Однако, законы Джима Кроу не позволяют блоку быть в списке со всеми наравне, перед блочным псевдоаргументом обязательно ставится &. Этот амперсанд не является методом или оператором, он выполняет роль маркера, который говорит Ruby о двух вещах:

  1. Этот аргумент — не аргумент, а блок метода.
  2. Всё, что передается методу под видом блока, нужно преобразовать в прок. Блоки оборачиваются в Proc.new {}; если это объект, у него вызывается метод to_proc.

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

:001 > hello = lambda { "hello" }
 => #<Proc:0xa04e1c4@(irb):1 (lambda)>
:002 > say = &hello
SyntaxError: (irb):2: syntax error, unexpected tAMPER

Во-вторых, поскольку методу можно передать один блок, в списке аргументов амперсандом можно отметить только один аргумент, и он должен быть последним.

В-третьих, в переменную, отмеченную амперсандом, автоматически попадает процедурный объект. Например, наш метод get можно записать и так:

def get(url, &captured_block)
  @blocks[url] = captured_block
end

В-четвертых (и это самое интересное), методу можно передать в качестве блока любой объект, у которого есть метод to_proc. Кандидат №1 — процедурный объект, его to_proc возвращает сам себя. Это позволяет пасовать блок (в виде объекта) по цепочке другим методам.

:001 > def pizza_each(type, &block)
:002?>   ingredients = case type
:003?>   when "Маргарита"
:004?>     %w(помидоры сыр базилик)
:005?>   when "Маринара"
:006?>     %w(помидоры чеснок орегано)
:007?>   end
:008?>   ingredients.each &block
:009?> end
 => nil
:010 > pizza_each("Маргарита") {|el| puts "Кладем #{el}"}
Кладем помидоры
Кладем сыр
Кладем базилик
 => ["помидоры", "сыр", "базилик"]

И эта цепочка может быть сколь угодно длинной:

:011 > tell_about_pizza = lambda { |x| puts "В этой пицце есть #{x}" }
 => #<Proc:0x92cd2ac@(irb):11 (lambda)>
:012 > pizza_each "Маринара", &tell_about_pizza
В этой пицце есть помидоры
В этой пицце есть чеснок
В этой пицце есть орегано
 => ["помидоры", "чеснок", "орегано"]

Что, если «забыть» приписать амперсанд? Ruby не ясновидящий, поэтому даже если мы передаем процедурный объект, и даже если он последний в списке, это еще не основание считать его блоком.

:014 > pizza_each "Маргарита", tell_about_pizza
ArgumentError: wrong number of arguments (2 for 1)

Как я уже говорил, если в качестве блока передается объект, Ruby неявно вызывает у него метод to_proc, чтобы внутрь метода уже попал процедурный объект. Проки и лямбды в этом случае прозрачны, т. к. превращаются сами в себя.

:013 > tell_about_pizza.to_proc.call "анчоусы"
В этой пицце есть анчоусы
 => nil

Но это означает, что определив в любом классе метод to_proc, объект этого класса можно передавать методу под видом блока. Главное, чтобы to_proc возвращал процедурный объект.

В качестве довольно глупого примера мы можем добавить такой метод классу String:

 :014 > class String
 :015?>   def to_proc
 :016?>     Proc.new { |x| puts(self + x) }
 :017?>   end
 :018?> end
  => nil
 :019 > pizza_each "Маргарита", &"Для ее приготовления нужны "
 Для ее приготовления нужны помидоры
 Для ее приготовления нужны сыр
 Для ее приготовления нужны базилик
  => ["помидоры", "сыр", "базилик"]

Но этому свойству всё-таки нашлось и полезное применение. Если передать в качестве блока символ (символьный объект), это равносильно передаче блока, в котором у аргумента будет вызываться одноименный метод. Такой вот синтаксический сахар, но никакой магии.

:001 > %w(моцарелла оливки грибы).map { |el| el.reverse }
 => ["аллерацом", "иквило", "ыбирг"]
:002 > %w(горгонзола розмарин салями).map &:reverse
 => ["алозногрог", "нирамзор", "имялас"]

Игры с замыканиями

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

Для демонстрации этого проведем свою версию эксперимента с кошкой Шрёдингера.

 :001 > cat_status = "Кошка жива"
  => "Кошка жива"
 :002 > print_cat_status = lambda { puts cat_status }
  => #<Proc:0x9ad4068@(irb):2 (lambda)>
 :003 > def schrodinger_box(observer)
 :004?>   cat_status = "Кошка уже надышалась газа"
 :005?>   observer.call
 :006?> end
  => nil
 :007 > schrodinger_box(print_cat_status)
 Кошка жива
  => nil

Важно то, что «захватывается» не статическая величина, которая была присвоена переменной в момент создания процедурного объекта, а именно сама переменная, обращение к которой происходит каждый раз, когда вызывается лямбда.

:008 > cat_status = "Кошка разбирает взрывное устройство"
 => "Кошка разбирает взрывное устройство"
:009 > schrodinger_box(print_cat_status)
Кошка разбирает взрывное устройство
 => nil

Возможен и обратный случай, когда замыкается локальное пространство метода. Обычно оно существует только в момент выполнения метода, после чего все локальные переменные перемещаются в мусорную корзину и уничтожаются дьявольским Сборщиком Мусора. Но если мы возвращаем из метода замыкание, «захваченные» переменные продолжают жить, пока существует само замыкание.

:001 > cat_status = "Мы ее теряем"
 => "Мы ее теряем"
:002 > def schrodinger_box
:003?>   cat_status = "А кошка-то жива!"
:004?>   Proc.new do
:005?>     cat_status += ' :)'
:006?>     puts cat_status
:007?>   end
:008?> end
 => nil
:009 > box = schrodinger_box
 => #<Proc:0x9eef918@(irb):4>
:010 > box.call
А кошка-то жива! :)
 => nil
:011 > box.call
А кошка-то жива! :) :)
 => nil
:012 > schrodinger_box.call
А кошка-то жива! :)
 => nil
:013 > box.call
А кошка-то жива! :) :) :)
 => nil

Короткая лямбда

Кроме метода lambda существует более короткий способ создания лямбды — оператор ->. С его использованием список аргументов выносится из блока и берется в круглые скобки вместо прямых.

Было

say_hello = lambda { |name| puts "Привет, #{name}!" }

стало

say_hello = ->(name) { puts "Привет, #{name}!" }

а учитывая, что скобки вокруг аргументов можно не писать, и вовсе:

say_hello = -> name { puts "Привет, #{name}!" }

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

Метод как объект

Итак, проки и лямбды возникли, чтобы решать две задачи:

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

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

Извлечение

Методы всегда привязаны к объекту, потому что обладают способностью «видеть» переменные этого объекта (@instance_variables). «Оторвать» метод можно с сохранением этой привязки или без нее. В первом случае у выбранного объекта вызывается метод method, который принимает в качестве аргумента имя извлекаемого метода и возвращает объект класса Method.

:001 > class MottoOfADay
:002?>   def initialize
:003?>     @motto = case rand(3)
:004?>       when 0 then "Будь друг, да не вдруг."
:005?>       when 1 then "Одним гусем поля не вытопчешь."
:006?>       when 2 then "Чужой дурак — веселье, а свой — бесчестье."
:007?>     end
:008?>   end
:009?>   def say_motto
:010?>     puts @motto
:011?>   end
:012?> end
 => nil
:013 > todays_motto = MottoOfADay.new
 => #<MottoOfADay:0x9e066b4 @motto="Одним гусем поля не вытопчешь.">
:014 > print_motto = todays_motto.method :say_motto
 => #<Method: MottoOfADay#say_motto>
:015 > print_motto.class
 => Method
:016 > print_motto.call
Одним гусем поля не вытопчешь.
 => nil

Как видим, метод-объект ведет себя точно так же, как и объект класса Proc. Забавно, мы даже можем сделать конверсию в процедурный объект с помощью уже известного метода to_proc:

:017 > proc_object = print_motto.to_proc
 => #<Proc:0x9e0b434 (lambda)>
:018 > proc_object.lambda?
 => true

Но кому может понадобиться метод в виде объекта? Не проще ли вызывать его как обычно, по имени? Проще, и на практике так обычно и поступают. Но теоретически наличие метод-объекта позволяет обращаться со всеми выполняемыми конструкциями в Ruby через один интерфейс — метод call. Это может быть полезно для DSL, в которых методы могут принимать на усмотрение пользователя либо блоки, либо уже имеющиеся в классе методы. Например, как это дает делать в Ruby on Rails семейство методов _filter:

class ApplicationController < ActionController::Base
  before_filter :require_login

  private

  def require_login
    #...
  end
end

или так:

class ApplicationController < ActionController::Base
  before_filter do |controller|
    #...
  end
end

Создание

В отличие от извлечения методов, их создание в объектах «на лету» (динамически) очень популярно в Ruby. Один из способов это сделать — вызов метода класса define_method, который принимает в качестве аргумента имя, а также блок, который становится телом метода.

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

Вот как можно избежать дублирования кода (потеряв при этом в его читаемости) при создании похожих методов:

:001 > class Friend
:002?>   %w(hello goodbye thanks).each do |action|
:003 >     say = lambda { |name| "#{action}, #{name}!" }
:004?>     define_method "say_#{action}", &say
:005?>   end
:006?> end
 => ["hello", "goodbye", "thanks"]
:007 > denny = Friend.new
 => #<Friend:0x8ea5328>
:008 > denny.say_hello "Alan"
 => "hello, Alan!"
:009 > denny.say_goodbye "Shirley"
 => "goodbye, Shirley!"

Итоговая таблица

Чтобы не запутаться в этих методах, блоках и лямбдах, вот все их свойства в одной таблице.

Свойства Метод Метод-объект Лямбда Прок Блок
является объектом, может передаваться как аргумент нет да да да нет
является замыканием нет нет да да да
создает локальную область видимости переменных да да да да да
видит переменные экземпляра (@variables) да да да/нет*) да/нет*) да/нет*)
считает аргументы да да да нет нет
return вызывает return у окружающего метода нет нет нет да да
*) — да, если определено в окружении экземпляра (например, внутри метода), за счет того, что является замыканием

Процедурные объекты на практике

Загрузочные хуки в Ruby on Rails

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

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

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

ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Base)

Она сигнализирует, что загрузка ActiveRecord завершена, и запускает все callback'и, повешенные на этот хук. Сам callback задается с помощью обычного блока:

ActiveSupport.on_load(:active_record) do
  self.time_zone_aware_attributes = true
  self.default_timezone = :utc
end

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

Если мы посмотрим на реализацию метода ActiveSupport.on_load

def self.on_load(name, options = {}, &block)
  if base = @loaded[name]
    execute_hook(base, options, block)
  else
    @load_hooks[name] << [block, options]
  end
end

то увидим, что там происходит обычное конвертирование блока в прок и сохранение его в хеше @load_hooks. Метод run_load_hooks просто находит все проки, сохраненные под конкретным названием хука, и по очереди вызывает их.