Как стать автором
Обновить

Строительные блоки Ruby

Время на прочтение 13 мин
Количество просмотров 19K
Автор оригинала: Yehuda Katz
Хвастаясь классными особенностями Ruby перед непосвященными (или перед партнером по языковому спаррингу), возбужденный рубист часто хватается за «мощный синтаксис блоков» в Ruby. К сожалению, для питониста или джависта остаются непонятными возможности пресловутого «мощного блочного синтаксиса» из-за отсутствия соответствующих механизмов в их языках.

Начнем с того, что мы обычно указываем на Rake, RSpec или Sinatra в качестве примеров удивительного использования блочного синтаксиса:

Copy Source | Copy HTML<br/>get "/hello" do<br/>  "Hello World"<br/>end <br/>
(см. www.sinatrarb.com/intro.htmlприм. перев.)

Питонисты обычно указывают на эквивалентный синтаксис в ответ:
Copy Source | Copy HTML<br/>@get('/hi')<br/>def hello():<br/>  return "Hello World"<br/> <br/>def hello() -> "/hi":<br/>  return "Hello World" <br/>

Хотя версия на Python может и уступает по красоте версии Ruby, но сказать «Ruby имеет больше возможностей» довольно трудно. Рубисты наоборот нивелируют аргумент большой семантической мощи сводя его к внешней красоте, когда используют этот пример из Sinatra.

Рубисты, питонисты и другие разработчики, работающие на ниве веб-разработки, используют общий язык JavaScript. Описывая блоки «внешним» людям, которые владеют JavaScript'ом, мы в качестве примера стремимся привести его функции. К сожалению, это только усиливает непонимание.

Похожая ситуация наблюдается со стороны Ruby, когда PHP или Java объявляет «добавление замыканий», многие из нас не перестают спрашивать «какого типа эти замыкания?»

Перейдем к сути


Давайте перейдем к сути дела и покажем лучший пример полезности блоков Ruby.
Copy Source | Copy HTML<br/>def append(location, data)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  File.open(path, "a") do |file|<br/>    file.puts YAML.dump(data)<br/>  end<br/> <br/>  return data<br/>end <br/>

Метод File.open принимает блок в качестве параметра. Он открывает новый файл (в режиме «append») и передает открытый файл в блок. Когда тот заканчивает работу, Ruby закрывает файл. Кроме этого, Ruby не просто закрывает файл, он гарантирует, что File будет закрыт, даже если выполнение блока завершается исключением. Давайте посмотрим на реализацию File в Rubinius:
Copy Source | Copy HTML<br/>def self.open(*args)<br/>  io = new *args<br/> <br/>  return io unless block_given?<br/> <br/>  begin<br/>    yield io<br/>  ensure<br/>    begin<br/>      io.close unless io.closed?<br/>    rescue StandardError<br/>      # nothing, just swallow them.<br/>    end<br/>  end<br/>end <br/>

Это означает, что вы можете заворачивать вездесущие идиомы типа try/catch/finally внутрь методов.
Copy Source | Copy HTML<br/># Без блоков<br/>def append(location, data)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  begin<br/>    file = File.open(path, "a")<br/>    file.puts YAML.dump(data)<br/>  ensure<br/>    file.close<br/>  end<br/> <br/>  return data<br/>end <br/>

Поскольку Ruby вызывает ensure даже когда исключение случается внутри блока, программист может быть уверен, что Ruby выполнит завершающую логику, спрятанную внутрь метода.
Этот пример демонстрирует хорошее качество реализации lambda-функций. Однако блоки в Ruby превращаются в нечто совершенно иное благодаря одной маленькой дополнительной особенности.
Copy Source | Copy HTML<br/>def write(location, data)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  File.open(path, "w") do |file|<br/>    return false if Digest::MD5.hexdigest(file.read) == data.hash<br/>    file.puts YAML.dump(data)<br/>  end<br/> <br/>  return true<br/>end <br/>

Представьте, что запись данных на диск требует довольно много ресурсов, и можно пропустить запись, если MD5 хеш содержания файла соответствует значению функции hash объекта data. Мы вернем false, если метод не произвел запись на диск и true в обратном случае.

Блоки Ruby поддерживают non-local-return (несколько ссылок), что означает, что return изнутри блока ведет себя идентично возврату из оригинального контекста блока. В этом случае возврат изнутри блока возвращает из метода write, но Ruby все равно вызывает ensure для закрытия файла.

Можно представить себе non-local-return как нечто подобное этому:
Copy Source | Copy HTML<br/>def write(location, data)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  File.open(path, "w") do |file|<br/>    raise Return.new(false) if Digest::MD5.hexdigest(file.read) == data.hash<br/>    file.puts YAML.dump(data)<br/>  end<br/> <br/>  return true<br/>rescue Return => e<br/>  return e.object<br/>end <br/>
, где Return — это Return = Struct.new(:object).

Конечно, такое должна поддерживать любая разумная реализация lambda-функций, но версия Ruby имеет то преимущество, что возврат изнутри блока выглядит так же, как возврат из обычного метода, и при этом требует гораздо меньше «блеска» для достижения эффекта. Эта особенность также помогает в случаях, когда уже используются rescue или ensure, избегая головоломных комбинаций.

Ruby поддерживает вызов super внутри блока. Представьте, что метод write был переопределен в подклассе, а тот же метод класса-родителя просто берет сырые данные из файла и пишет их в лог.
Copy Source | Copy HTML<br/>def write(location, data)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  File.open(path, "w") do |file|<br/>    file_data = file.read<br/>    super(location, file_data)<br/>    return false if Digest::MD5.hexdigest(file_data) == data.hash<br/>    file.puts YAML.dump(data)<br/>  end<br/> <br/>  return true<br/>end <br/>

В чистом сценарии lambda-функции нам потребовалось бы хранить ссылку на self, чтобы потом использовать ее внутри lambda:
Copy Source | Copy HTML<br/>def write(location, data)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  this = self<br/>  File.open(path, "w") do |file|<br/>    file_data = file.read<br/> <br/>    # воображаемая конструкция Ruby нужная без<br/>    # non-local-super<br/>    this.super.write(location, file_data)<br/>    raise Return.new(false) if Digest::MD5.hexdigest(file_data) == data.hash<br/>    file.puts YAML.dump(data)<br/>  end<br/> <br/>  return true<br/>rescue Return => e<br/>  return e.object<br/>end <br/>

В Ruby вы можете также вызвать yield в блок, полученный методом, внутри другого блока. Представьте, что метод write вызывается с блоком, который выбирает какие данные использовать в зависимости от того, является ли файл исполнимым:
Copy Source | Copy HTML<br/>def write(location)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  File.open(path, "w") do |file|<br/>    file_data = file.read<br/>    super(location)<br/>    data = yield file<br/>    return false if Digest::MD5.hexdigest(file_data) == data.hash<br/>    file.puts YAML.dump(data)<br/>  end<br/> <br/>  return true<br/>end <br/>

Это можно вызвать через:
Copy Source | Copy HTML<br/>write("/path/to/file") do |file|<br/>  if file.executable?<br/>    "#!/usr/bin/env ruby\nputs 'Hello World!'"<br/>  else<br/>    "Hello World!"<br/>  end<br/>end <br/>

В чистом lambda-языке, мы бы принимали блок как нормальный аргумент функции и вызывали бы его внутри замыкания:
Copy Source | Copy HTML<br/>def write(location, block)<br/>  path = Pathname.new(location)<br/>  raise "Location does not exist" unless path.exist?<br/> <br/>  this = self<br/>  File.open(path, "w") do |file|<br/>    file_data = file.read<br/> <br/>    # воображаемая конструкция Ruby, нужная без<br/>    # non-local-super<br/>    this.super.write(location, file_data)<br/>    data = block.call(file)<br/>    raise Return.new(false) if Digest::MD5.hexdigest(file_data) == data.hash<br/>    file.puts YAML.dump(data)<br/>  end<br/> <br/>  return true<br/>rescue Return => e<br/>  return e.object<br/>end <br/>

Реальное преимущество подхода Ruby заключается в том, что код внутри блока был бы идентичен в случае, если бы метод не принимал бы блок. Рассмотрим такой же метод, принимающий File вместо location:
Copy Source | Copy HTML<br/>def write(file)<br/>  file_data = file.read<br/>  super(file)<br/>  data = yield file<br/>  return false if Digest::MD5.hexdigest(file_data) == data.hash<br/>  file.puts YAML.dump(data)<br/>  return true<br/>end <br/>

Без блока код Ruby выглядит точно так же. Это означает, что Ruby-программисты могут легче переносить повторяющийся код в методы, принимающие блоки, без переписывания большого количества кода. Это также означает, что использование блока не прерывает нормальную логику и можно создавать новые конструкции «управляющей логики», которые ведут себя почти идентично встроенным логическим конструкциям типа if и while.

Rails хорошо применяет это в respond_to, предлагая удобный синтаксис согласования контента:
Copy Source | Copy HTML<br/>def index<br/>  @people = Person.find(:all)<br/> <br/>  respond_to do |format|<br/>    format.html # default action is render<br/>    format.xml { render :xml => @people.xml }<br/>  end<br/>end <br/>

Благодаря тому, как работают блоки в Ruby, вы можете также вернуться из любого из блоков format:
Copy Source | Copy HTML<br/>def index<br/>  @people = Person.find(:all)<br/> <br/>  respond_to do |format|<br/>    format.html { redirect_to(person_path(@people.first)) and return }<br/>    format.xml { render :xml => @people.xml }<br/>    format.json { render :json => @people.json }<br/>  end<br/> <br/>  session[:web_service] = true<br/>end <br/>

Мы вернулись из HTML format после редиректа, что позволило нам выполнить дополнительное действие (установить :web_service в сессии) для других случаев (XML и JSON MIME-типы).

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

Так почему блоки в Ruby лучше?


Если вы забрались так далеко, давайте рассмотрим еще один вариант использования блоков в Ruby: синхронизацию мютексов.

Java поддерживает синхронизацию через специальное ключевое слово synchronized:
Copy Source | Copy HTML<br/>class Example {<br/>  final Lock lock = new Lock();<br/> <br/>  void example() {<br/>    synchronized(lock) {<br/>      // разные опасные штуки<br/>    }<br/>  }<br/>} <br/>

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

Аналогично, Python требовал использование try/finally до версии Python 2.5, когда была добавлена специальная языковая функция для обработки идиомы try/finally:
Copy Source | Copy HTML<br/>class Example:<br/>  # старый вариант<br/>  def example(self):<br/>    lock.acquire()<br/>    try:<br/>      ... access shared resource<br/>    finally:<br/>      lock.release() # разблокировать, независимо от обстоятельств<br/> <br/>  # новый вариант<br/>  def example(self):<br/>    with lock:<br/>      ... access shared resource <br/>

В случае 2.5, объект, переданный в with, должен реализовать специальный протокол (включая методы __enter__ и __exit__), поэтому выражение with не может быть использовано как общецелевые и легковесные блоки Ruby.
Ruby представляет такую же концепцию использования метода, принимающего блок:
Copy Source | Copy HTML<br/>class Example<br/>  @@lock = Mutex.new<br/> <br/>  def example<br/>    @@lock.synchronize do<br/>      # разные опасные штуки<br/>    end<br/>  end<br/>end <br/>

Важно отметить, что synchronize — это нормальный Ruby-метод. Оригинальная версия, написанная на чистом Ruby, выглядит следующим образом:
Copy Source | Copy HTML<br/>def synchronize<br/>  lock<br/>  begin<br/>    yield<br/>  ensure<br/>    unlock<br/>  end<br/>end <br/>

Тут есть все признаки того, что мы успели обсудить выше. Она блокирует объект, вызывает блок и после удостоверяется, что блокировка снята. Это означает, что если программист Ruby возвращает результат изнутри блока, synchronize сработает правильно.

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

Postscript


Исторически сложилось, что я писал свои посты без большого количества ссылок, в первую очередь опасаясь их устаревания. Я получаю все больше запросов на аннотации в моих постах, поэтому начну делать это. Дайте мне знать, если вы думаете, что мои аннотации в этом посте были полезны и свободно предлагайте что найдете полезным в этой сфере.

Некоторые полезные комментарии после статьи



James Edward Gray II:
При использовании Pathname, можно перевести:

Copy Source | Copy HTML<br/>File.open(path, “a”) do |file|<br/># …<br/>end <br/>
в:
Copy Source | Copy HTML<br/>path.open(“a”) do |file|<br/># …<br/>end <br/>


Colin Curtin:
Кое-что, о чем нужно помнить про non-local-return: блок должен иметь доступ к контексту, из которого вы хотите вернуться.

Copy Source | Copy HTML<br/>def a<br/>  yield<br/>end<br/>a{ return  0 } # => LocalJumpError: unexpected return<br/> <br/>def c<br/>  yield<br/>end<br/> <br/>def b<br/>  c { return 1 }<br/>end<br/>b # => 1<br/> <br/>def d<br/>  lambda{return 2}.call<br/>end<br/>d # => 2 <br/>


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
Комментарии 46
Комментарии Комментарии 46

Публикации

Истории

Работа

Программист Ruby
12 вакансий
Ruby on Rails
11 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн