Я хочу добавить Kernel.rand
вот так:
# I try something like
mod = Module.new do
def rand(*args)
p "do something"
super(*args)
end
end
Kernel.prepend(mod)
# And I expect this behaviour
Kernel.rand #> prints "do something" and returns random number
rand #> prints "do something" and returns random number
Object.new.send(:rand) #> prints "do something" and returns random number
К сожалению, приведенный выше код не работает так, как я хочу. Добавление Kernel.singleton_class
тоже не работает
Использовать функцию prepend
не обязательно, любые предложения, которые помогут добиться желаемого поведения, приветствуются.
Кстати, каков ваш фактический вариант использования, т. е. почему вы вообще хотите исправить Kernel.rand
?
По сути, я хочу отслеживать каждый вызов rand
. Похоже, простое обновление до Ruby 3.2 устранило проблему, и теперь я получаю ожидаемые результаты.
Имейте в виду, что помимо Kernel.rand
есть еще Random.rand , Random#rand и (через stdlib) также SecureRandomrand
.
Я использовал версию 2.7 и не знал об изменениях, внесенных в версию 3.0, поэтому удалил свой ответ.
Kernel
методы, такие как rand
, или Math
, такие как cos
, определяются как так называемые функции модуля (см. модуль_функция), что делает их доступными как в обоих случаях, так и в других случаях.
... (публичные) одноэлементные методы:
Math.cos(0) # <- `cos' called as singleton method
#=> 1.0
... и (частные) методы экземпляра:
class Foo
include Math
def calc
cos(0) # <- `cos' called from included module
end
end
foo = Foo.new
foo.calc
#=> 1.0
foo.cos(0) # <- not allowed
# NoMethodError: private method `cos' called for #<Foo:0x000000010e3ab510>
Чтобы добиться этого, одноэлементный класс Math
не просто включает Math
(который превратил бы все его методы в одноэлементные). Вместо этого каждый метод «функции модуля» определяется дважды: в модуле и в одноэлементном классе модуля:
Math.private_instance_methods(false)
#=> [:ldexp, :hypot, :erf, :erfc, :gamma, :lgamma, :sqrt, :atan2, :cos, ...]
# ^^^
Math.singleton_class.public_instance_methods(false)
#=> [:ldexp, :hypot, :erf, :erfc, :gamma, :lgamma, :sqrt, :atan2, :cos, ...]
# ^^^
В результате добавление другого модуля к Math
или исправление Math
в целом повлияет только на (частный) метод экземпляра и, следовательно, только на классы, включающие Math
. Это не повлияет на метод cos
, который был определен отдельно в одноэлементном классе Math
. Чтобы также исправить этот метод, вам также придется добавить свой модуль к классу Singleton:
module MathPatch
def cos(x)
p 'cos called'
super
end
end
Math.prepend(MathPatch) # <- patch classes including Math
Math.singleton_class.prepend(MathPatch) # <- patch Math.cos itself
Который дает:
Math.cos(0)
# "cos called"
#=> 1.0
А также:
foo.calc
# "cos called"
#=> 1.0
Однако в качестве побочного эффекта метод экземпляра становится общедоступным:
foo.cos(0)
# "cos called"
#=> 1.0
Я выбрал Math
в качестве примера, потому что он менее интегрирован, чем Kernel
, но те же правила применяются к «глобальным функциям» из Kernel
.
Что особенного в Kernel
, так это то, что он также включен в main
, который является контекстом выполнения Ruby по умолчанию, т. е. вы можете вызывать rand
без явного получателя.
Хорошо, я наконец-то заработал. Вот почему раньше это не работало. По сути, я так и делал ruby mod = Module.new do def rand(*args) p "..." super(*args) end end [Kernel, Kernel.singleton_class].each { _1.prepend(mod) }
И это просто не работало должным образом в MRI 2.7.4. Однако в Ruby 3.2.3 это работает так, как ожидалось. Итак, я думаю, они что-то изменили в реализации Ruby.
@vetements в Ruby 2.7.4 вам необходимо include
/ prepend
, прежде чем модуль будет включен. Начиная с Ruby 3.0, изменения в списках предков будут применяться задним числом.
спасибо, что рассказали! Я помню, как читал об этом в книге «Рубин под микроскопом», но не знал, что это исправлено в Руби 3.0.
В дополнение к ожидаемому поведению вы можете добавить фактическое (неожиданное) поведение/выход вашего кода.