Добавление функции модуля ядра глобально

Я хочу добавить 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 не обязательно, любые предложения, которые помогут добиться желаемого поведения, приветствуются.

В дополнение к ожидаемому поведению вы можете добавить фактическое (неожиданное) поведение/выход вашего кода.

Stefan 06.03.2024 12:00

Кстати, каков ваш фактический вариант использования, т. е. почему вы вообще хотите исправить Kernel.rand?

Stefan 06.03.2024 12:09

По сути, я хочу отслеживать каждый вызов rand. Похоже, простое обновление до Ruby 3.2 устранило проблему, и теперь я получаю ожидаемые результаты.

vetements 06.03.2024 14:20

Имейте в виду, что помимо Kernel.rand есть еще Random.rand , Random#rand и (через stdlib) также SecureRandomrand.

Stefan 06.03.2024 15:17

Я использовал версию 2.7 и не знал об изменениях, внесенных в версию 3.0, поэтому удалил свой ответ.

Cary Swoveland 06.03.2024 18:41
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Пошаговое руководство по созданию собственного Slackbot: От установки до развертывания
Шаг 1: Создание приложения Slack Чтобы создать Slackbot, вам необходимо создать приложение Slack. Войдите в свою учетную запись Slack и перейдите на...
1
5
96
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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 06.03.2024 14:14

@vetements в Ruby 2.7.4 вам необходимо include / prepend, прежде чем модуль будет включен. Начиная с Ruby 3.0, изменения в списках предков будут применяться задним числом.

Stefan 06.03.2024 15:27

спасибо, что рассказали! Я помню, как читал об этом в книге «Рубин под микроскопом», но не знал, что это исправлено в Руби 3.0.

vetements 06.03.2024 17:23

Другие вопросы по теме