Pull to refresh

RubyConf 2010: настоящее и будущее Руби (I)

Reading time 8 min
Views 1.1K
Original author: Yukihiro Matsumoto
Я предлагаю вам прочитать перевод второй части доклада автора Руби, Yukihiro Matsumoto, на RubyConf 2010, опубликованных в его блоге на японском языке. Первая часть представляет из себя краткий исторический обзор предыдущих конференций RubyConf и не представляет интереса к публикации.

За помощь в переводе с японского огромное спасибо Владимиру Садовникову.


Будущее Ruby: Ruby 2.0


В этот раз я представлю несколько новых функций, которые будут в Ruby:
  • Типажи (traits)1
  • Комбинации методов
  • Именованные аргументы
  • Пространства имен

Я вполне уверен, что до сих пор об этом не было речи. Отличие заключается в том, что:
  • Эти функции — не просто идеи, которые однажды могут быть реализованы. Так нужно было делать с самого начала.
  • Соответствующий код уже закоммичен в trunk.

1 Прим. пер.: для нетривиальных технических терминов я буду давать английское название в скобках и ссылку на Википедию.

Типажи

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

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

Впрочем, для того, чтобы представить функциональность модуля в другом ключе, типажи реализуют примеси (mixins) при помощи иного метода, нежели включения (include) [других модулей — здесь и далее в квадратных скобках прим. пер.]. Так как этот метод является более гибким, то возможность включения модулей убрана окончательно.

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

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

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

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

module American
  attr_accessor :address
end

module Japanese
  attr_accessor :address
end

class JapaneseAmerican
  include American
  include Japanese
end

JapaneseAmerican.new.address # which address?

p JapaneseAmerican.ancestors # => [JapaneseAmerican, Japanese, American, Object, Kernel]

В этом примере метод-атрибут address содержится и в модуле American, и в Japanese, перекрываясь по желанию разработчика или случайно из-за незнания им языка. В результате методы будут вызываться просто в последовательности, соответствующей порядку наследования.

«Хорошо обученный» Руби-программист [Rubyist], взглянув на этот код, конечно же, поймет, что будет вызван метод address из модуля Japanese.

В данный момент Ruby [MRI 1.9] использует парадигму «модуль не должен быть унаследован дважды, даже если родительский класс уже включает этот модуль» (прим. авт.: похоже, в MacRuby сделано так же. В 1.9 была попытка это изменить, но для работы YARV необходимо, чтобы модуль не появлялся среди предков дважды.). Так что если вы включаете модуль в класс, родительский класс которого уже включает этот модуль, то он не появится среди предков там, где вы ожидаете.

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

Эти проблемы (отчасти) решаются методом смешивания (mix).

Почему смешивание? Потому что:
  • Методы класса определены в текущем модуле / включаемом модуле.
  • При включении модуль не становится предком.
  • Совпадение названий методов возбуждает исключение.
  • Если включение модуля с совпадающими названиями методов все же необходимо, их можно переименовать.
  • Константы можно включать или не включать (по умолчанию они не включаются).

Таким образом, смешивание реализуется так:
  • Попытка добавить дубликат метода будет вызывать ошибку, потому что разрешить конфликт автоматически нельзя.
  • Имена методов можно изменить, указав это явно.
  • Модули все еще включаются в цепочку наследования странным образом. (Эта проблема так и не решена, удачи. *усмешка*)

Например, в этом коде есть методы с одинаковыми именами, и поэтому смешение не сработает:
module American
  attr_accessor :address
end

module Japanese
  attr_accessor :address
end

class JapaneseAmerican
  mix American
  mix Japanese # => address conflict!
end

Для того, чтобы смешение сработало, нужно явно разрешить конфликты имен:
class JapaneseAmerican
  mix American, :address => :us_address
  mix Japanese, :address => :jp_address
end

Теперь методы, которые назывались одинаково, получили разные имена.

Вы можете спросить, почему бы вместо того, чтобы усложнять язык, не добавить возможность передачи параметров включения в метод include? Потому что более короткое название метода означает, что он предпочтительнее к использованию. (прим. перев.: именно поэтому все интроспективные методы, которые потенциально могут делать код сложным к пониманию, имеют подробные имена вроде «instance_variable_defined?»).

Nakada [вероятно, Nobuyoshi Nakada, один из разработчиков ядра Ruby] уже начал разработку типажей, необходимых для реализации метода mix, в момент анонсирования этой функции на RubyKaigi [японская Ruby-конференция], так что патч уже готов.

Впрочем, различные презентации не рассматривают следующие проблемы, по которым еще будет необходимо принять решение:
  • [В этом месте были два пункта, которые я так и не смог перевести. Извините.]
  • Не предусмотрено никаких способов разрешения конфликтов среди переменных-членов класса. Так как переменные-члены дочерних классов добавляются незаметно для родительского, при реализации класса, который смешивается с модулем, придется учитывать нежелательные имена для переменных-членов и избегать их. Существует патч для 1.9, который делает переменные-члены вида @_foo или @__foo закрытыми, но в текущей ситуации трудно понять, как именно следует это реализовать или, возможно, стоит отказаться от подобной функции вообще. Этой проблеме придется уделить больше внимания после введения смешивания.

Комбинации методов

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

Предложение заключается во введении метода prepend. Я уже слышу вопрос «У нас уже есть include и mix, что, теперь нужен еще один метод?», и я считаю, что он действительно необходим.

Этот метод позволяет дописать другой метод «перед» текущим классом, чтобы добавить какие-либо функции.
module Foo
  def foo
    p :before 
    super
    p :after 
  end
end

class Bar
  def foo
    p :foo
  end
  prepend Foo
end

Bar.new.foo # :before, :foo, :after

Если в модуле Foo определен метод foo, и он же определен в текущем классе (или модуле), то prepend обернет вызов метода foo текущего класса, используя метод foo модуля.

Идея метода prepend исходит от Yehuda Katz, который, кроме того, разработчик Rails; он считает, что великолепно было бы заменить им alias_method_chain в Rails, и я с ним согласен.

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

Именованные аргументы

Люди склонны забывать назначение аргументов, и в особенности необязательных. Например, метод public_instance_methods принимает необязательный аргумент, который означает «если передано false, то вернуть методы и родительского класса тоже», или наоборот, я всегда это забываю. (На самом деле, true.)

Например,
aClass.public_instance_methods (include_super: false)

Так гораздо проще запомнить.

В Ruby 1.9 именованные аргументы представляют из себя просто последний аргумент с хешем как значением по умолчанию, и расширение грамматики, при помощи которого инициализируется этот хеш. (прим. перев.: мало кто знает, что в 1.9 можно сделать метод вроде def a(opts={}); end и вызывать его как a(go: true, what: "string").

Новая возможность 2.0 — простой способ указать такую запись при определении метода. Например:

Вызов:
1.step (by: 2, to: 20) do |i|
  p i
end

Вызываемый метод:
def step (by: step, to: limit)
  ...
end

После принятия [сообществом] формы вызова с хешем добавление именованных аргументов станет, в сущности, небольшим изменением.

Пространства имен

Если вас интересуют технические детали, стоит взглянуть на презентацию Shugo Maeda, которая также была на RubyConf.

Классы в Ruby являются открытыми. Это значит, что в любой момент можно добавить метод в существующий класс. Такая методика изменения классов называется «monkey patching».

Как мне кажется, этот термин был порожден цепочкой «guerilla patching» [guerilla — партизан] -> «patching gorilla» -> «monkey patching». Конечно, если в таком языке, как Ruby, изменять классы легко, то я уверен, что такую возможность нужно предоставлять. Именно поэтому доклад DHH [David Heinemeier Hanson, автор Rails] «Будущее без monkey patching» я называю «Будущее без свободы изменений». [здесь была цитата из Мела Гибсона, но мой мозговой парсер ее не осилил].

С одной стороны, свобода хороша, но с другой она может оказывать слишком большое влияние. Конечно, можно переопределить сложение для целых чисел так, чтобы 1+2 было равно 42, но большая часть программ перестанет работать из-за побочных эффектов.

Проблема заключается в том, что все подобные изменения действуют глобально, на всю программу. Помещая изменения в закрытую область видимости, можно получить удобный и безопасный метод «свободных изменений» [freedom of patching].

Такой «областью видимости» мог послужить предложенный ранее Selector Namespace Classbox; в этот раз Selector Namespace, реализованный коллегой Maeda, можно назвать «уточнением» [Refinement].

Например, вот такая программа:
class Integer
  def / (other)
    return quo (other)
  end
end

p 1 / 2 # => (1 / 2)

Подобное переопределение оператора деления (/) для того, чтобы вернуть результат в виде рациональной дроби (прим.пер.: в данном случае возвращается объект класса Rational), является частью стандартной библиотеки mathn. Однако, код, который ожидает, что результатом целочисленного деления будет также целое число, может перестать работать из-за такого изменения.

В этом исследовании предлагается ввести уточнения (название может измениться). Грамматика может выглядеть так:
module MathN
  refine Integer do
    def / (other)
      return quo (other)
    end
  end

  p 1 / 2 # => (1 / 2)
end

p 1 / 2 # => 0

Уточнение используется как часть модуля. Модуля. Это большое достижение.

Внутри модуля можно уточнять существующие классы, и такие изменения будут видны только в пределах пространства имен. Таким образом, внутри модуля MathN 1 / 2 вернет Rational(1 / 2), а вне модуля — обычное частное.

Отличия уточнений от classbox заключается в том, что несмотря на то, что лексическая область видимости уже кончилась, уточнение еще в действии; по этой же причине во многих языках программирования используется динамическая область видимости. Конечно, уточнения должны работать императивно, а не лексически.

Пространство имен — это модуль, использующий метод using:
module Rationalize
  using MathN
  p 1 / 2 # => (1 / 2)
end

p 1 / 2 # => 0

В модуле Rationalize тоже доступно уточнение, определенное в MathN.

Кроме того, если какой-то метод бессмысленно определять как метод класса, то его можно определить внутри другого метода. В пределах метода это будет работать так же, как и уточнение: таким образом метод можно сделать полностью закрытым. (прим. перев.: в Ruby 1.9 метод, заданный подобным образом, становится методом-членом класса, то есть вложенность не имеет никакого значения.)
class Foo
   def foo
      def bar
        ...
      end
      bar # works
   end

   def quux
     bar  # does not work
   end
end

Это изменение, над которым работает Maeda достаточно большое и сложное; впрочем, изменения быстро интегрируются в основное дерево [trunk], а пока можно поапплодировать коллеге Maeda за этот великолепный патч.

P.S. Этот перевод был далеко не простым, и я знаю, что многие фразы сведут с ума любого криптолингвиста, но мне совесть не позволила вырывать из текста лишь те произвольные части, которые было перевести проще всего. Надеюсь, что большая часть интересных вещей будет понятна по немногим выжившим комментариям и исходному коду. И я обещаю, что перевод следующей части будет намного понятнее.
Tags:
Hubs:
+26
Comments 23
Comments Comments 23

Articles