Проки и лямбды
Послемыслия и работа над ошибками
- добавлены раздел «Метод как объект» и сводная таблица (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 о двух вещах:
- Этот аргумент — не аргумент, а блок метода.
- Всё, что передается методу под видом блока, нужно преобразовать в прок.
Блоки оборачиваются в
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
просто находит все проки, сохраненные
под конкретным названием хука, и по очереди вызывает их.