Refinemets в Ruby: эволюция и текущий статус
Refinements были добавлены в Ruby 2.0 в далеком 2013 году и должны были помочь улучшить модульность кода и сделать расширения классов и модулей более безопасными. Refinements позволяют задавать область видимости для monkey-patch, делать его локальным и скрывать для остального приложения. Это в первую очередь важно для разработчиков библиотек и позволяет избежать конфликтов между их monkey-patch‘ами.
Следующий пример (из документации) иллюстрирует синтаксис:
class C
end
module M
refine C do
def foo
puts "C#foo in M"
end
end
end
using M
x = C.new
x.foo # prints "C#foo in M"
Начальная концепция Shugo Maeda
Впервые Refinements были представлены на RubyConf 2010 в докладе Shugo Maeda (видео). Качество звука плохое, докладчик не очень опытный и просто читает слайды, поэтому можно сразу к ним и перейти (слайды). Стоит смотреть видео с 15:30 (33-й слайд), когда докладчик переходит к существующим аналогичным решениям в других языках и переходит, собственно, к синтаксису Refinements.
Shugo Maeda называет следующие существующие решения:
- selector namespace
- SmallScript (.Net), ECMAScript 4
- Lexically scoped
- classboxes
- Squeak, Java (non-official extention)
- Dynamically scoped (called local rebinding)
Семантика Refinements описывается следующим образом:
Module#refine
:
refine(klass, &block)
- additional or overriding methods of class are defined in block
- a set of such methods is called a refinement
- activated only in the receiver module and scoped where the module is imported by
using
refine
can also be invoked on classes
Module#using
:
using(mod)
using
imports refinements defined in mod- refinements are activated only in a file, module, class or method where
using
is invoked- lexically scoped
- supports reopen and inheritance
Refinements предназначены для:
- Refinements of build-in classes
- Internal DSLs
- Nested methods
В основных чертах это не сильно отличается от финальной версии Refinements, хотя есть и мелкие несоответствия. Например, в этой ранней версии:
- можно переоткрывать только классы,
- перед
using
нужно явно подключить модуль черезinclude
Проработка концепции
Бурные обсуждение новой фичи, ее синтаксиса и особенностей начались в том же 2010 году. В этом приняли участие Shugo Maeda, Matz и другие сочувствующие лица. Отметился Jeremy Evans, разработчик Sequel и много чего другого, и Charles Nutter, ведущий разработчик JRuby. Все это вылилось в достаточно длинный тред, чтение которого весьма доставляет, хотя и требует времени.
Через два года в декабре 2012 стало окончательно понятно, что новая фича слишком сложна и с ней связано слишком много неясностей и нюансов. Как кто-то прокомментировал: “Если уж разработчики самого Ruby не понимают, как это должно работать, то как же быть обычным людям?”. Например, были неясности в работе методов рефлексии, наследования и подключение модулей, области видимости. Трудно было оценить влияние на производительность.
Несмотря на предложения отложить релиз фичи на следующую версию Ruby 2.1 Matz настоял на включении Refinements в ближайший релиз Ruby 2.0 хотя и в сильно урезанном виде:
using
is only allowed at top level.- refined methods are called only after
using
. - or within blocks given to
refine
. - if you pass the proc to
refine
e.g.refine(C,&b)
refined methods may not be called from b. It’s implementation dependent. - refinements are not available in subclasses, nor in reopened classes/modules.
- refinements are not available from
module_eval
/class_eval
.
В таком виде Refinements были зарелизины в Ruby 2.0 в 2012 году и в Release Notes попал следующий абзац:
“In addition, albeit as an experimental feature, 2.0.0 includes Refinements, which adds a new concept to Ruby’s modularity.”
Дальнейшее развитие
В череде следующих релизах регулярно появлялись доработки и исправления связанные с Refinements.
Ruby 2.1 (2013-12-25)
Refinements перестала быть экспериментальной фичей. Убрали ограничение на использование using
только в top level контексте. Теперь Refinemets можно подключать в любом модуле и классе.
- The method (
main.using
) activates refinements in the ancestors of the argument module to support refinement inheritance byModule#include
. - (New method:)
Module#using
, which activates refinements of the specified module only in the current class or module definition
Ruby 2.4 (2016-12-25)
Значительным улучшением стала возможность refine‘ить не только классы но и модули. Core team перешла к полировке фичи. Начата (но не закончена) работа над поддержкой специфичных случаях - непрямые вызова методов (Symbol#to_proc
, …), Kernel#binding
etc.
- Refinements is enabled at method by
Symbol#to_proc
. - Refinements is enabled with
Kernel#send
andBasicObject#__send__
. Module#refine
accepts a module as the argument now
Ruby 2.5 (2017-12-25)
Продолжена работа над поддержкой особых случаев - в этот раз добавлена только поддержка в интерполяции строк.
- refinements take place in string interpolations.
Ruby 2.6 (2018-12-25)
Продолжена работа на поддержкой непрямых вызовов методов и передачи блока (&:symbol
)
- Refinements take place at block passing.
- Refinements take place at
Kernel#public_send
. - Refinements take place at
Kernel#respond_to?
.
Ruby 2.7 (еще не зарелизен)
Хотя это и не попало в список изменений для preview версии, но были устранены последние недоработки. Добавили поддержку Refinements в следующих методах:
Kernel#method
Kernel#instance_method
С выходом Ruby 2.7 мы получим наконец завершенную фичу готовую к использованию, несмотря на ряд известных багов (#14744, #14012, #13752, #13446, #11704, #9580)
Заключение
Как видим, фича получилась весьма сложной. Достаточно только посмотреть на тесты проекта RubySpec (Module#using
, Module#refine
) и станет ясно, как много разных сложных комбинаций и вариантов применения можно найти и Principle Of Least Surprise здесь ни разу не помогает. Предположить навскидку, как должны вести себя Refinements в нетривиальной ситуации достаточно непростая задача.
Если прикинуть, то понадобилось 8 лет, чтобы закончить эту фичу, хотя все еще есть некоторые шероховатости. В JRuby Refinements были добавлены только в последнем релизе. А в коде Rails refine
/using
применили сравнительно недавно - в версии Rails 5.1 вышедшей в 2017 году.
Таким образом, Refinements до сих пор не нашла широкого применения даже среди популярных библиотек и остается редко используемой маргинальной фичей языка.
Полезные ссылки
- Yehuda Katz “Ruby 2.0 Refinements in Practice”
- Актуальная документация (Ruby 2.6)
- Alexandre Bergel. Scoping Changes with Method Namespaces
- Alexandre Bergel, Stephane Ducasse, and Roel Wuyts. Classboxes: A Minimal Module Model Supporting Local Rebinding
- Тред #4085 с обсуждением с Matz
- RubySpec: Module#using
- RubySpec: Module#refine