I'm trying to override a Rails helper method that's defined like this:
class Foo
module Bar
def orig
# orig code
end
alias o orig
module_function :o
module_function :orig
end
end
So that I can override and add functionality to orig and o something like this:
def orig
# new code
# super (run orig code)
end
alias o orig
I've looked through several different monkey patching methods but they don't seem to work. I believe the module_function is what's throwing it off.
Anyone know how I can achieve this?
Here's a workaround. You can re-open the module, make an unbound reference to the original instance method, then redefine it to call the original method (with some altered behavior).
First, the original definition:
module Foo
def bar(arg)
"self=#{self}, arg=#{arg}"
end
module_function :bar
end
Next, reopening and redefining the method:
module Foo
OrigBarMethod = instance_method(:bar)
def bar(arg)
Foo::OrigBarMethod.bind(self).call(arg + 1)
end
module_function :bar
end
puts Foo.bar(1) # => "self=Foo, arg=2"
I use bind(self) so that the original method can still make use of self, for example:
class MyClass
include Foo
end
MyClass.new.send(:bar, 1) # => "self=#<MyClass:0x00007fb66a86cbf8>, arg=2"
Pretty much any circumstance where you would in the past have used monkey-patching can nowadays be solved with inheritance and Module#prepend:
module Foo
def bar(arg)
"self=#{self}, arg=#{arg}"
end
module_function :bar
end
module FooExtension
def bar(arg)
super(arg + 1)
end
end
[Foo, Foo.singleton_class].each do |mod|
mod.prepend FooExtension
end
Foo.bar(1) #=> "self=Foo, arg=2"
class MyClass
include Foo
end
MyClass.new.bar(1) #=> "self=#<MyClass:0x00007fb66a86cbf8>, arg=2"
Related
I've been reading this article on the difference between include & extend in ruby.
If I have this module, I understand how the first and second methods of the module will be used in the class. What I don't understand is how the class << self will be used by include or extend.
module Direction
def straight
puts "going straight!"
end
def turn
puts "turning!"
end
class << self
def stop
puts "stopping!"
end
end
end
# This will work because `include` brings them in as instance methods
class Car
include Direction
end
Car.new.straight
Car.new.turn
# ---------------------
# Now this will also work because `extend` brings them in as class methods
class Car
extend Direction
end
Car.straight
Car.turn
# ---------------------
Now, the issue is, doing Car.stop or Car.new.stop will always result in an error:
/Users/<name>/Projects/ruby-testing/main.rb:34:in `<main>': undefined method `stop' for Car:Class (NoMethodError)
Why are class methods not carried over via include and extend?
I started thinking about this because of my research into the [forwardable source code at line 119].(https://github.com/ruby/ruby/blob/master/lib/forwardable.rb#L119)
Thank you for any help you may have!
Update from Answer Below
The following was an example given:
module Direction
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def stop
puts 'stopping!'
end
end
def straight
puts "going straight!"
end
def turn
puts "turning!"
end
end
class Car
include Direction
end
This I understand now, and I understand how I can implement class methods from a module into a class using def self.included(base). My question is, if we used extend inside of Car instead of include, would we still be able to get at those class methods using def self.included(base)?
When you define a method with class << self you are defining a class method. It's the same as defining the methed like this:
class Foo
def self.foo
puts 'foo'
end
# the above definition is the same as doing:
class << self
def foo
puts 'foo'
end
end
end
The above shows 2 ways of defining class methods which are called directly on the class and not on instances of the class. You might use the 2nd syntax if you want to define only class methods or several of them inside of the class << self block. But either style has the same result.
Since you've defined a class method on the Direction module, include or extend will not inherit the class method of that module. This is the expected behavior.
If you want to use inheritance with class methods from a module, you should do it like this which is explained further down in the article you've linked
module Direction
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def stop
puts 'stopping!'
end
end
def straight
puts "going straight!"
end
def turn
puts "turning!"
end
end
class Car
include Direction
end
Now calling class methods on Car will inherit as defined in the Direction class.
Car.stop
stopping!
=>nil # calling a method will return nil unless the method returns a value.
However always be careful using inheritance of any kind as Ruby is a dynamic language. So if you do the above code and then later redefine this method:
module Direction
module ClassMethods
def stop
puts 'go!'
end
end
end
Guess what will happen if you do this:
Car.stop
Since the method was defined inside Direction module, when the method gets called on Car it will be calling the method from the Direction module.
Car.stop
go!
=>nil
Updated based on comments:
If you prefer to use extend vs include you would need to do this instead:
module Direction
def self.extended(base)
base.extend(ClassMethods)
end
module ClassMethods
def stop
puts 'stopping!'
end
end
end
class Car
extend Direction
end
In this example, all the methods which were inherited from the module are "copied" to the class extending them. This avoids the problem of possible result of redefining the module method which I warned about when using include previously in my answer.
But you may want to look at answers to this question for ideas about when and why to use either case.
I'm confused about using "include" vs "extend, after searching for hours all I got is that module methods used with instance of the class including the module, and module methods used with the class itself when the class extending the module of those methods.
but this didn't help me to figure out, why this code give error when commenting the extend module line in "#extend Inventoryable"
while work when uncomment it, here's the code
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
#extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
the output is
store2.rb:52:in `<main>': undefined method `create' for Shirt:Class (NoMethodError)
while when uncomment the "extend Inventoryable" to make the code work:
module Inventoryable
def create(attributes)
object = new(attributes)
instances.push(object)
return object
end
def instances
#instances ||= []
end
def stock_count
#stock_count ||= 0
end
def stock_count=(number)
#stock_count = number
end
def in_stock?
stock_count > 0
end
end
class Shirt
extend Inventoryable
include Inventoryable
attr_accessor :attributes
def initialize(attributes)
#attributes = attributes
end
end
shirt1 = Shirt.create(name: "MTF", size: "L")
shirt2 = Shirt.create(name: "MTF", size: "M")
puts Shirt.instances.inspect
makes the code work and output the following
[#<Shirt:0x0055792cb93890 #attributes={:name=>"MTF", :size=>"L"}>, #<Shirt:0x0055792cb937a0 #attributes={:name=>"MTF", :size=>"M"}>]
it's kinda confusing, but all I need to know, is why I need to extend the module in order to avoid the error ?, and how to edit this code to make it work without the extend method ? , what's left in the code that still depends on the extend ?
When you extend a module, the methods in that module become "class methods"**. So, when you extend Inventoryable, create becomes available as a method on the Shirt class.
When you include a module, the methods in that module become "instance methods"**. So, when you include Inventoryable, create is not available on the Shirt class (but is available on an instance of Shirt).
To make create available on the Shirt class when using include, you can use the included hook. That might look something like:
module Inventoryable
module ClassMethods
def create
puts "create!"
end
end
module InstanceMethods
end
def self.included(receiver)
receiver.extend ClassMethods
receiver.include InstanceMethods
end
end
Then if you do:
class Shirt
include Invetoryable
end
You can do:
> Shirt.create
create!
=> nil
** The ruby purists in the crowd will correctly point out that, in ruby, everything is an instance method and that there are no class methods. That is formally 100% correct, but we'll use the colloquial meaning of class and instance methods here.
When you extend a module in a class, you get the module's methods exposed as class methods but if you include the module then you get the module's method as instance methods, in your example for you to be able to call create method of Inventoryable class you need to invoke it using an instance of Shirt class (if you include the module)
shirt1 = Shirt.new(attributes).create(attributes)
Without more info I can't tell what you are trying to do but you need to redesign the initialize and create methods to decide where or what to do in those methods.
I'll try to explain it using a simple example
module A
def test
puts "ok"
end
end
class B
include A
end
class C
extend A
end
puts C.test # here you invoke the method against the class itself
puts B.new.test #here you create an instance to do it
Hope it helps.
At the end of the day, it's really simple:
C.include(M) makes the current superclass of C the superclass of M and M the superclass of C. In other words, it inserts M into C's ancestry chain.
obj.extend(M) is (roughly) the same as obj.singleton_class.include(M).
In Ruby, I understand that ::ClassName for calling class at base module. For example, here is my code:
module HHT
module V1
module OfflineCheckIn
class PutOfflineCheckInProductsApi < ApplicationApi
put 'offline' do
ActiveRecord::Base.transaction do
OfflineCheckIn.create(check_in_param) # exception here
end
end
end
end
end
end
When I run, I meet exception:
NoMethodError (undefined method `create' for
HHT::V1::OfflineCheckIn:Module)
As I understand, Rails understand that OfflineCheckIn currently inside module HHT::V1::OfflineCheckIn, so I must call at base class ::OfflineCheckIn. Thing that I don't understand is: at another controller, some previous programmer implements same way with me, but he doesn't need to call :: before model.
So my question is: when we don't need to use :: before class and rails can understand that is base class ?
Thanks
You have to call class as ::ClassName if in your hierarchy there's a class/module with the same name, to differentiate between them, for example:
class Foo; end
module Bar
class Foo; end # this is another Foo
def self.a
puts ::Foo == Foo
end
end
module FooBar
def self.a
puts ::Foo == Foo
end
end
Bar.a # => false
FooBar.a # => true
Here we have ::Foo and ::Bar::Foo, but shorthand Foo points to one of them depending on context.
Also it does not matter if the entities are classes or modules, both are just assigned as a value for a constant:
module Foo; end
module Bar
Foo = "a string"
def self.baz
puts Foo.class
end
end
puts Foo.class # => Module
Bar.baz # => String
In Ruby, when a method is aliased, the alias points to the body of the original method. So even if you redefine the original method, the alias will continue to use the original definition.
class Foo
def bar
"bar"
end
alias :saloon :bar
end
class Foo
def bar
"BAR"
end
end
puts Foo.new.saloon
will return 'bar' and not 'BAR'. Is there any way to get saloon to use the new definition of bar?
EDIT: I should have been more clear. The example was just an illustration of the issue - it's not the actual problem I need to solve. The issue is more complex when you have chained aliases, for example, in rails' core. E.g. perform_action is aliased by benchmarking module, and then also by flash module. So now a call to perform_action is actually calling perform_action_with_flash which does it's thing, then effectively calls perform_action_with_benchmarking which then calls the original perform_action. If I want to override perform_action_with_benchmarking (even though I agree it's a bad idea - please let's not get into a discussion of that as it's besides the point), I can't because it has been aliased, and as far as I can tell the alias is pointing to what is essentially a copy of the original perform_action_with_benchmarking, so even if I redefine it, there's no effect.
Just re-establish the alias:
class Foo
def bar
"bar"
end
alias :saloon :bar
end
class Foo
def bar
"BAR"
end
alias :saloon :bar
end
puts Foo.new.saloon # => "BAR"
class Foo
def bar
"bar"
end
def saloon
bar
end
end
This is not an alias at all, but it works as you want.
Yes and no. Either coreyward or Sony Santos's solutions work fine. What you need to know is why your coded didn't work the way you though.
alias makes a new name for the function as is appears when the method is invoked. This is not a pointer, but a new way of referring to something. It allows us to do something like this:
class Foo
def bar
"bar"
end
alias :speakeasy :bar
end
class Foo
def bar(secret_code = false)
return speakeasy if secret_code == "A friend of Al"
"Closed because of prohibition!"
end
end
puts Foo.new.bar #=> "Closed because of prohibition!"
puts Foo.new.bar "A friend of Al" #=> "bar"
The old bar still exists, it just a little harder to access now.
Here is another answer, but you have to do some additional steps: collect the aliases before overriding, and realias after:
class Class
def get_aliases method_name
original_proc = instance_method method_name
aliases = []
instance_methods.each do |meth|
# if the methods have different names but they're the same, they're aliased
if meth != method_name.to_s && original_proc == instance_method(meth)
aliases << meth
end
end
aliases
end
end
class Foo
def bar
"bar"
end
alias :saloon :bar
end
class Foo
aliases = get_aliases :bar
def bar
"BAR"
end
aliases.each { |a| alias_method a, :bar }
end
puts Foo.new.saloon #=> BAR
BTW, if anyone can strip off one of that steps, may I know it! :)
I'm hoping to set up a default path for logging, relative to the path of the file using the log, something like this:
# /path/to/lib/bar.rb
class Bar
def settings_file_path
File.dirname(File.expand_path(__FILE__))
end
end
# /path/to/app/models/foo.rb
class Foo < Bar
end
Foo.new.settings_file_path
Ideal output:
# => /path/to/app/models
Actual output:
# => /path/to/lib
Because FILE references the file where it's written, not where it's being called from, it's returning the bar.rb file, but I want something like this to return the path of the foo.rb file, even though the method is defined in Bar.
Anybody have any suggestions?
The simplest would be something like this:
# foo.rb
class Foo
def self.my_file
#my_file
end
end
# bar.rb
class Bar < Foo
#my_file = __FILE__
end
# main.rb
require_relative 'foo'
require_relative 'bar'
p Bar.my_file
#=> "/Users/phrogz/Desktop/bar.rb"
However, you could parse the caller in a self.inherited hook like so:
# foo.rb
class Foo
class << self
attr_accessor :_file
end
def self.inherited( k )
k._file = caller.first[/^[^:]+/]
end
end
# bar.rb
class Bar < Foo
end
# main.rb
require_relative 'foo'
require_relative 'bar'
p Bar._file
#=> "/Users/phrogz/Desktop/bar.rb"
I'm not certain how robust or portable that parsing is; I suggest you test it.
N.B. My Bar inherits from Foo, the reverse of your question. Be not confused by the differences in our setups.
caller.first[/^[^:]+/] will not work on Windows because absolute path there looks like $DRIVE:$PATH (example C:/Windows/system32)
Instead caller.first[/^[^:]+/] use caller_locations.first.absolute_path
Ruby 2.7 adds Module#const_source_location
const_source_location(:Foo)
Use $0 instead of __ FILE__ :)