Начнем с того, что мы обычно указываем на Rake, RSpec или Sinatra в качестве примеров удивительного использования блочного синтаксиса:
Copy Source | Copy HTML
get "/hello" do
"Hello World"
end
(см. www.sinatrarb.com/intro.html — прим. перев.)Питонисты обычно указывают на эквивалентный синтаксис в ответ:
Copy Source | Copy HTML
@get('/hi')
def hello():
return "Hello World"
def hello() -> "/hi":
return "Hello World"
Хотя версия на Python может и уступает по красоте версии Ruby, но сказать «Ruby имеет больше возможностей» довольно трудно. Рубисты наоборот нивелируют аргумент большой семантической мощи сводя его к внешней красоте, когда используют этот пример из Sinatra.
Рубисты, питонисты и другие разработчики, работающие на ниве веб-разработки, используют общий язык Javascript. Описывая блоки «внешним» людям, которые владеют Javascript'ом, мы в качестве примера стремимся привести его функции. К сожалению, это только усиливает непонимание.
Похожая ситуация наблюдается со стороны Ruby, когда PHP или Java объявляет «добавление замыканий», многие из нас не перестают спрашивать «какого типа эти замыкания?»
Перейдем к сути
Давайте перейдем к сути дела и покажем лучший пример полезности блоков Ruby.
Copy Source | Copy HTML
def append(location, data)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
File.open(path, "a") do |file|
file.puts YAML.dump(data)
end
return data
end
Метод
File.open принимает блок в качестве параметра. Он открывает новый файл (в режиме «append») и передает открытый файл в блок. Когда тот заканчивает работу, Ruby закрывает файл. Кроме этого, Ruby не просто закрывает файл, он гарантирует, что File будет закрыт, даже если выполнение блока завершается исключением. Давайте посмотрим на реализацию File в Rubinius:Copy Source | Copy HTML
def self.open(*args)
io = new *args
return io unless block_given?
begin
yield io
ensure
begin
io.close unless io.closed?
rescue StandardError
# nothing, just swallow them.
end
end
end
Это означает, что вы можете заворачивать вездесущие идиомы типа try/catch/finally внутрь методов.
Copy Source | Copy HTML
# Без блоков
def append(location, data)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
begin
file = File.open(path, "a")
file.puts YAML.dump(data)
ensure
file.close
end
return data
end
Поскольку Ruby вызывает
ensure даже когда исключение случается внутри блока, программист может быть уверен, что Ruby выполнит завершающую логику, спрятанную внутрь метода.Этот пример демонстрирует хорошее качество реализации lambda-функций. Однако блоки в Ruby превращаются в нечто совершенно иное благодаря одной маленькой дополнительной особенности.
Copy Source | Copy HTML
def write(location, data)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
File.open(path, "w") do |file|
return false if Digest::MD5.hexdigest(file.read) == data.hash
file.puts YAML.dump(data)
end
return true
end
Представьте, что запись данных на диск требует довольно много ресурсов, и можно пропустить запись, если MD5 хеш содержания файла соответствует значению функции hash объекта data. Мы вернем false, если метод не произвел запись на диск и true в обратном случае.
Блоки Ruby поддерживают non-local-return (несколько ссылок), что означает, что return изнутри блока ведет себя идентично возврату из оригинального контекста блока. В этом случае возврат изнутри блока возвращает из метода
write, но Ruby все равно вызывает ensure для закрытия файла.Можно представить себе non-local-return как нечто подобное этому:
Copy Source | Copy HTML
def write(location, data)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
File.open(path, "w") do |file|
raise Return.new(false) if Digest::MD5.hexdigest(file.read) == data.hash
file.puts YAML.dump(data)
end
return true
rescue Return => e
return e.object
end
, где Return — это Return = Struct.new(:object).Конечно, такое должна поддерживать любая разумная реализация lambda-функций, но версия Ruby имеет то преимущество, что возврат изнутри блока выглядит так же, как возврат из обычного метода, и при этом требует гораздо меньше «блеска» для достижения эффекта. Эта особенность также помогает в случаях, когда уже используются
rescue или ensure, избегая головоломных комбинаций.Ruby поддерживает вызов
super внутри блока. Представьте, что метод write был переопределен в подклассе, а тот же метод класса-родителя просто берет сырые данные из файла и пишет их в лог.Copy Source | Copy HTML
def write(location, data)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
File.open(path, "w") do |file|
file_data = file.read
super(location, file_data)
return false if Digest::MD5.hexdigest(file_data) == data.hash
file.puts YAML.dump(data)
end
return true
end
В чистом сценарии lambda-функции нам потребовалось бы хранить ссылку на self, чтобы потом использовать ее внутри lambda:
Copy Source | Copy HTML
def write(location, data)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
this = self
File.open(path, "w") do |file|
file_data = file.read
# воображаемая конструкция Ruby нужная без
# non-local-super
this.super.write(location, file_data)
raise Return.new(false) if Digest::MD5.hexdigest(file_data) == data.hash
file.puts YAML.dump(data)
end
return true
rescue Return => e
return e.object
end
В Ruby вы можете также вызвать
yield в блок, полученный методом, внутри другого блока. Представьте, что метод write вызывается с блоком, который выбирает какие данные использовать в зависимости от того, является ли файл исполнимым:Copy Source | Copy HTML
def write(location)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
File.open(path, "w") do |file|
file_data = file.read
super(location)
data = yield file
return false if Digest::MD5.hexdigest(file_data) == data.hash
file.puts YAML.dump(data)
end
return true
end
Это можно вызвать через:
Copy Source | Copy HTML
write("/path/to/file") do |file|
if file.executable?
"#!/usr/bin/env ruby\nputs 'Hello World!'"
else
"Hello World!"
end
end
В чистом lambda-языке, мы бы принимали блок как нормальный аргумент функции и вызывали бы его внутри замыкания:
Copy Source | Copy HTML
def write(location, block)
path = Pathname.new(location)
raise "Location does not exist" unless path.exist?
this = self
File.open(path, "w") do |file|
file_data = file.read
# воображаемая конструкция Ruby, нужная без
# non-local-super
this.super.write(location, file_data)
data = block.call(file)
raise Return.new(false) if Digest::MD5.hexdigest(file_data) == data.hash
file.puts YAML.dump(data)
end
return true
rescue Return => e
return e.object
end
Реальное преимущество подхода Ruby заключается в том, что код внутри блока был бы идентичен в случае, если бы метод не принимал бы блок. Рассмотрим такой же метод, принимающий File вместо location:
Copy Source | Copy HTML
def write(file)
file_data = file.read
super(file)
data = yield file
return false if Digest::MD5.hexdigest(file_data) == data.hash
file.puts YAML.dump(data)
return true
end
Без блока код Ruby выглядит точно так же. Это означает, что Ruby-программисты могут легче переносить повторяющийся код в методы, принимающие блоки, без переписывания большого количества кода. Это также означает, что использование блока не прерывает нормальную логику и можно создавать новые конструкции «управляющей логики», которые ведут себя почти идентично встроенным логическим конструкциям типа if и while.
Rails хорошо применяет это в respond_to, предлагая удобный синтаксис согласования контента:
Copy Source | Copy HTML
def index
@people = Person.find(:all)
respond_to do |format|
format.html # default action is render
format.xml { render :xml => @people.xml }
end
end
Благодаря тому, как работают блоки в Ruby, вы можете также вернуться из любого из блоков format:
Copy Source | Copy HTML
def index
@people = Person.find(:all)
respond_to do |format|
format.html { redirect_to(person_path(@people.first)) and return }
format.xml { render :xml => @people.xml }
format.json { render :json => @people.json }
end
session[:web_service] = true
end
Мы вернулись из HTML format после редиректа, что позволило нам выполнить дополнительное действие (установить :web_service в сессии) для других случаев (XML и JSON MIME-типы).
Выше мы продемонстрировали несколько особенностей блоков в Ruby.
return, yield и super вместе можно увидеть крайне редко. Программисты Ruby обычно используют одну или несколько из этих конструкций внутри блоков, просто потому, что их использование кажется естественным.Так почему блоки в Ruby лучше?
Если вы забрались так далеко, давайте рассмотрим еще один вариант использования блоков в Ruby: синхронизацию мютексов.
Java поддерживает синхронизацию через специальное ключевое слово
synchronized:Copy Source | Copy HTML
class Example {
final Lock lock = new Lock();
void example() {
synchronized(lock) {
// разные опасные штуки
}
}
}
По сути, Java обеспечивает специальную конструкцию для реализации идеи, что определенный кусок кода должен быть выполнен только один раз для заданного экземпляра объекта синхронизации. Поскольку Java предлагает специальную конструкцию, вы можете вернуть изнутри кода синхронизации и рантайм Java выполнит соответствующие обработки.
Аналогично, Python требовал использование
try/finally до версии Python 2.5, когда была добавлена специальная языковая функция для обработки идиомы try/finally:Copy Source | Copy HTML
class Example:
# старый вариант
def example(self):
lock.acquire()
try:
... access shared resource
finally:
lock.release() # разблокировать, независимо от обстоятельств
# новый вариант
def example(self):
with lock:
... access shared resource
В случае 2.5, объект, переданный в
with, должен реализовать специальный протокол (включая методы __enter__ и __exit__), поэтому выражение with не может быть использовано как общецелевые и легковесные блоки Ruby.Ruby представляет такую же концепцию использования метода, принимающего блок:
Copy Source | Copy HTML
class Example
@@lock = Mutex.new
def example
@@lock.synchronize do
# разные опасные штуки
end
end
end
Важно отметить, что
synchronize — это нормальный Ruby-метод. Оригинальная версия, написанная на чистом Ruby, выглядит следующим образом:Copy Source | Copy HTML
def synchronize
lock
begin
yield
ensure
unlock
end
end
Тут есть все признаки того, что мы успели обсудить выше. Она блокирует объект, вызывает блок и после удостоверяется, что блокировка снята. Это означает, что если программист Ruby возвращает результат изнутри блока,
synchronize сработает правильно.Этот пример демонстрирует ключевую мощь блоков Ruby: они могут с легкостью заменить конструкции языка. То есть Ruby-программист может взять небезопасный код, вставить его в блок синхронизации, и код после этого будет работать безопасно.
Postscript
Исторически сложилось, что я писал свои посты без большого количества ссылок, в первую очередь опасаясь их устаревания. Я получаю все больше запросов на аннотации в моих постах, поэтому начну делать это. Дайте мне знать, если вы думаете, что мои аннотации в этом посте были полезны и свободно предлагайте что найдете полезным в этой сфере.
Некоторые полезные комментарии после статьи
James Edward Gray II:
При использовании Pathname, можно перевести:
Copy Source | Copy HTML
File.open(path, “a”) do |file|
# …
end
в:Copy Source | Copy HTML
path.open(“a”) do |file|
# …
end
Colin Curtin:
Кое-что, о чем нужно помнить про non-local-return: блок должен иметь доступ к контексту, из которого вы хотите вернуться.
Copy Source | Copy HTML
def a
yield
end
a{ return 0 } # => LocalJumpError: unexpected return
def c
yield
end
def b
c { return 1 }
end
b # => 1
def d
lambda{return 2}.call
end
d # => 2
ecin:
Помните, что разные способы создания замыканий (Proc.new, proc, lambda) не всегда эквивалентны друг другу:
innig.net/software/ruby/closures-in-ruby.rb
…
Rit Li:
Люблю ruby блоки. Спасибо за статью.
По поводу “Rails vs Django”, есть три вещи, в которых Rails выигрывает:
1) Convention over Configuration.
У Django нет больших файлов конфигурации, только один файл settings.py. Так что Django — это фреймворк с “Easy Configuration,” не “Convention over Configuration.”
2) REST
Rails действительно охватывает REST. Семи-экшеновый контроллер замечателен. Django не имеет встроенного механизма resource/route для REST. Однажды начав REST, вы не пойдете назад.
3) Эко система в Rails
Rails плагины есть для всего. Плюс, есть коммерческая поддержка, книги, блоги, скринкасты и хостинги для Rails. Django действительно этого не хватает.
Постскрипты переводчика
PS могу запостить только в свой блог, потому что не хватает кармы для специализированного, но надеюсь, она будет полезна и я перемещу его, например, в Ruby :)
PPS Если надо — добавлю временные метки для комментариев и комментарии про питон (я его не очень хорошо знаю)
PPPS Пожалуйста, не ругайте меня за содержание статьи — ругайте только за перевод :)



комментарии (46)