Method super calls parent method based on __method__.
How to call parent method based on __callee__?
class SomeLogic
DICTIONARY = {
new_method_1: 'dictionary value 1',
new_method_2: 'dictionary value 2'
}
def initialize(method_name)
#method_name = method_name
end
def call
DICTIONARY[#method_name]
end
end
module M
extend ActiveSupport::Concern
def base_method
logic = SomeLogic.new(__callee__).call
if logic
logic
elsif defined?(__callee__)
super # here analogue of super needed
else
{}
end
end
alias_method :new_method_1, :base_method
alias_method :new_method_2, :base_method
alias_method :new_method_3, :base_method
end
class A
prepend M
def new_method_3
'foo'
end
end
Expected results:
A.new.new_method_1 # dictionary value 1
A.new.new_method_2 # dictionary value 2
A.new.new_method_3 # foo
Current results:
A.new.new_method_1 # dictionary value 1
A.new.new_method_2 # dictionary value 2
A.new.new_method_3 # no superclass method `base_method' for A
If you change
class A
prepend M
to
class A
include M
you will get the desired output
Okay so this is extremely convoluted but it will work as requested
class SomeLogic
DICTIONARY = {
new_method_1: 'dictionary value 1',
new_method_2: 'dictionary value 2'
}
def initialize(method_name)
#method_name = method_name
end
def call
DICTIONARY[#method_name]
end
end
module M
def self.prepended(base)
base.extend(ClassMethods)
end
def base_method(&block)
SomeLogic.new(__callee__).call ||
block&.call || # ruby < 2.3 block && block.call
{}
end
module ClassMethods
def method_added(method_name)
if M.instance_methods.include?(method_name)
orig = M.instance_method(method_name)
M.remove_method(method_name)
new = self.instance_method(method_name)
self.prepend(Module.new do
define_method(method_name) do
result = orig.bind(self).call(&new.bind(self))
end
end)
M.define_method(method_name, &orig.bind(M))
end
end
end
alias_method :new_method_1, :base_method
alias_method :new_method_2, :base_method
alias_method :new_method_3, :base_method
alias_method :new_method_4, :base_method
end
class A
prepend M
def new_method_3
'foo'
end
end
class B
prepend M
end
So when you define a new method we check to see if that method is already defined by M. If so then we capture that UnboundMethod remove the method from M, then we capture the new definition as an UnboundMethod then define the new method in an anonymous Module as Ms implementation passing the new method implementation as a proc to Ms implementation and prepend this module to the defining Class, then we place Ms implementation back into M so other classes still work.
Result:
p A.new.new_method_1
#=> 'dictionary value 1'
p A.new.new_method_2
#=> 'dictionary value 2'
p A.new.new_method_3
#=> 'foo'
p A.new.new_method_4
#=> {}
p B.new.new_method_3
#=> {}
If you would prefer include over prepend then M could look like
module M
def self.included(base)
super
base.extend(ClassMethods)
end
def base_method
if logic = SomeLogic.new(__callee__).call
logic
elsif self.class.instance_methods(false).include?(__callee__)
send(__callee__,true)
else
{}
end
end
module ClassMethods
def method_added(method_name)
return if #_adding_method # avoid stack level issues
if M.instance_methods.include?(method_name)
new = instance_method(method_name)
#_adding_method = true # avoid stack level issues
define_method(method_name) do |bypass_parent=false|
return super() unless bypass_parent
new.bind(self).call
end
#_adding_method = false
end
end
end
end
Related
There is a general way of adding class methods from Module via its included hook, and following extending base class with ClassMethods submodule. This way is described in book "Metaprogramming Ruby 2: Program Like the Ruby Pros". Here is an example from there:
module CheckedAttributes
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("##{attribute}", value)
end
define_method attribute do
instance_variable_get "##{attribute}"
end
end
end
end
class Person
include CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
But what is the reason of including the almost empty module first, and then extending its includer with one more module? Why not extend the class right the way with target module itself?
module CheckedAttributes
def attr_checked(attribute, &validation)
define_method "#{attribute}=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("##{attribute}", value)
end
define_method attribute do
instance_variable_get "##{attribute}"
end
end
end
class Person
extend CheckedAttributes
attr_checked :age do |v|
v >= 18
end
end
Is code above totally equal to initial example from this book? Or there are any pitfalls?
I have no idea where you took this code from, but this pattern involving ClassMethods is normally used in the cases when you want to alter both class and eigenclass to avoid the necessity to call both include Foo and extend Bar.
module Named
def self.included(base)
base.extend ClassMethods
end
def describe
"Person is: #{name}"
end
module ClassMethods
def name!
define_method "name=" do |value|
raise 'Invalid attribute' unless validation.call(value)
instance_variable_set("#name", value)
end
define_method "name" do
instance_variable_get "#name"
end
end
end
end
class Person
include Named
name!
end
p = Person.new
p.name = "Trump"
p.describe #⇒ "Person is: Trump"
In your example, it makes zero sense.
I'm trying to achieve something that is for sure possible but I'm not able to put it into to find it from the docuemntation.
In a nutshell, I would like to define methods dynamically:
Initial point:
class Foo < Bar
def baz
RecordLoader.for(Baz).load(object.baz_id)
end
def qux
RecordLoader.for(Quz).load(object.qux_id)
end
end
class Bar
end
I would like to be able to change it to
class Foo < Bar
record_loader_for :baz
record_loader_for :qux
end
class Bar
def self.record_loader_for(attribute)
define_method attribute.to_s do
# What is missing here?
end
end
end
I'm trying to figure out how I can use the value of attribute to write something like
RecordLoader.for(attribute.to_s.classify.constantize). # <- attribute is local to the class
.load(object.send("#{attribute.to_s}_id")) # <- object is local to the instance
You can go with class_eval and generate your method into string:
def self.record_loader_for(attribute)
class_eval <<~RUBY, __FILE__ , __LINE__ + 1
def #{attribute}
RecordLoader.for(#{attribute.to_s.classify}).load(#{attribute}_id)
end
RUBY
end
but in fact, define_method should work too, ruby will save closure from the method call:
require 'active_support'
require 'active_support/core_ext'
require 'ostruct'
class RecordLoader
def self.for(cls)
new(cls)
end
def initialize(cls)
#cls = cls
end
def load(id)
puts "loading #{#cls} id #{id}"
end
end
class Baz; end
class Bar
def object
OpenStruct.new(baz_id: 123, qux_id:321)
end
def self.record_loader_for(attribute)
define_method attribute.to_s do
RecordLoader.for(attribute.to_s.classify.constantize).
load(object.send("#{attribute.to_s}_id"))
end
end
end
class Foo < Bar
record_loader_for :baz
record_loader_for :qux
end
Foo.new.baz
class_eval is slower to define method, but resulting method executes faster and does not keep references to original closure context, define_method is the opposite - defines faster, but method runs slower.
Is it possible to call a method defined in a module, if that method has been overridden in a class.
Class A
include bmodule
def greeting
super if some_condition_is_true
end
end
module bmodule
included do
has_many :greeters
def greeting
puts 'hi'
end
end
end
A.new.greeting needs to hit bmodule's greeting if some_condition_is_true is true
I tried prepending and including the module, it did not work. Is it possible to do so?
You have to save the original method before overriding it:
Class A
include bmodule
alias_method :original_greeting, :greeting
def greeting
original_greeting if some_condition_is_true
end
end
It's the example of the doc https://apidock.com/ruby/Module/alias_method
module B
def x
1
end
end
class A
include B
def x
super + 1
end
end
puts A.new.x
# => 2
The included do block with Rails concerns is so you can call class methods on the base. So I don't think you need to use it in this case.
Yes, you can do that, and you almost have it right. Just 1) Capitalize Bmodule so ruby doesn't scream at you, 2) lowercase class when defining A, 3) include ActiveSupport::Concern if you're using included, and 4) Move the greeting method out of the included block. The included block is for running things at the class level, and instance method definitions should not be inside it.
module Bmodule
extend ActiveSupport::Concern
included do
has_many :greeters
end
def greeting
puts 'hi'
end
end
class A
include Bmodule
def greeting
super if some_condition_is_true
end
end
A.new.greeting
You could use the method Method#super_method, which provides a great deal of flexibility.
module M1
def meth(arg)
yield arg
end
end
module M2
def meth(arg)
yield arg
end
end
class C
include M1
include M2
def meth(arg)
yield arg
end
def test(cond, &block)
case cond
when :C
meth(cond, &block)
when :M2
method(:meth).super_method.call(cond, &block)
when :M1
(method(:meth).super_method).super_method.call(cond, &block)
end
end
end
C.ancestors
#=> [C, M2, M1, Object, Kernel, BasicObject]
c = C.new
c.test(:C) { |m| "meth is from #{m}" }
#=> "meth is from C"
c.test(:M2) { |m| "meth is from #{m}" }
#=> "meth is from M2"
c.test(:M1) { |m| "meth is from #{m}" }
#=> "meth is from M1"
If for some reason you wanted to use prepend rather than include, C.ancesors would be as follows:
class C
prepend M1
prepend M2
end
C.ancestors
#=> [M2, M1, C, Object, Kernel, BasicObject]
so you would just modify test accordingly.
I wish to use a method in the controller:
class Hash
def sort_by_array a; Hash[sort_by{|k, _| a.index(k) || length}] end
end
But after placing the code in the controller, I receive an error: class definition in method body
I tried removing the class Hash, and second end, and have also tried
class Hash
def self.class.sort_by_array a; Hash[sort_by{|k, _| a.index(k) || length}] end
end
But I still can't get it to stop erroring
For reference, here is the controller:
class StaticPagesController < ApplicationController
def main
class Hash
def self.class.sort_by_array a; Hash[sort_by{|k, _| a.index(k) || length}] end
end
#languages = Listing.group_by(&:language)
#languages.sort_by_array(#languages)
end
end
That error occurs when you define a class inside the method of another class. i.e. you were probably doing something like below:
class SomeClass
def some_method
class Hash
def sort_by_array(a)
end
end
end
end
Assuming you want to extend the functionality of Hash objects by adding a method sort_by_array, then you can do monkey-patching like below:
Solution (Simple):
you can only define "instance" methods. If you want to define also "class" methods, see "Advanced" solution below.
lib/extensions/hash.rb
module Extensions
module Hash
def sort_by_array(a)
sort_by do |k, _|
a.index(k) || length
end
end
end
end
i.e. let's say another class you want to extend functionality:
lib/extensions/active_record/base.rb
module Extensions
module ActiveRecord
module Base
def say_hello_world
puts 'Hello World!'
end
end
end
end
config/initializers/extensions.rb
Hash.include Extensions::Hash
ActiveRecord::Base.include Extensions::ActiveRecord::Base
Usage:
# rails console
some_array = [:a, :c, :b]
some_hash = { a: 1, b: 2, c: 3 }
some_hash.sort_by_array(some_array)
# => [[:a, 1], [:c, 3], [:b, 2]]
user = User.find(1)
user.say_hello_world
# => 'Hello World!'
Solution (Advanced):
now allows both "class" and "instance" methods to be defined:
lib/extensions/hash.rb
module Extensions
module Hash
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
# define your Hash "class methods" here inside ClassMethods
module ClassMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.extended(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.singleton_methods - instance_methods).include? method_name
# end
# end
end
# define your Hash "instance methods" here inside InstanceMethods
module InstanceMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.included(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.instance_methods - instance_methods).include? method_name
# end
# end
def sort_by_array(a)
sort_by do |k, _|
a.index(k) || length
end
end
end
end
end
i.e. let's say another class you want to extend functionality:
lib/extensions/active_record/base.rb
module Extensions
module ActiveRecord
module Base
def self.included(base)
base.extend ClassMethods
base.include InstanceMethods
end
module ClassMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.extended(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.singleton_methods - instance_methods).include? method_name
# end
# end
def say_hello_mars
puts 'Hello Mars!'
end
end
module InstanceMethods
# commented out because not yet fully working (check update later)
# # feel free to remove this part (see P.S. for details)
# def self.included(base)
# instance_methods.each do |method_name|
# raise NameError, "#{method_name} method already defined!" if (base.instance_methods - instance_methods).include? method_name
# end
# end
def say_hello_world
puts 'Hello World!'
end
end
end
end
end
config/initializers/extensions.rb
Hash.include Extensions::Hash
ActiveRecord::Base.include Extensions::ActiveRecord::Base
Usage:
# rails console
some_array = [:a, :c, :b]
some_hash = { a: 1, b: 2, c: 3 }
some_hash.sort_by_array(some_array)
# => [[:a, 1], [:c, 3], [:b, 2]]
user = User.find(1)
user.say_hello_world
# => 'Hello World!'
ActiveRecord::Base.say_hello_mars
# => 'Hello Mars!'
P.S, arguably you won't need to raise an error if a method is already defined, but this is just my personal taste to prevent "bugs" (i.e. if for example some "gems" you used also defined same exact methods name but having different functionality, of which you have no control of). Feel free to remove them though.
Place it in a separate file. This would extend the base class Hash and would allow you to use in the whole application.
The easiest would be putting the code into config/initializers/hash.rb and restarting the server.
Or, better, similarly to how Rails does it (e.g. https://github.com/rails/rails/tree/master/activesupport/lib/active_support/core_ext):
Put your code here: lib/core_ext/hash/sort_by_array.rb
And then either add this path to autoload, or require it manually from where you'd like to use it, like this:
require "core_ext/hash/sort_by_array".
I have Memoize module, that provides methods for caching of class and instance methods.
module Memoize
def instance_memoize(*methods)
memoizer = Module.new do
methods.each do |method|
define_method method do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
end
end
prepend memoizer
end
def class_memoize(*methods)
methods.each do |method|
define_singleton_method method do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
end
end
end
This is an example of how I use it:
class Foo
extend Memoize
instance_memoize :instance_method1, :instance_method2
class_memoize :class_method1, :class_method2
...
end
Please advice how to avoid code duplication in this module.
One might define a lambda:
λ = lambda do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
And. then:
define_method method, &λ
Please be aware of an ampersand in front of λ, it is used to notice define_method about it’s receiving a block, rather than a regular argument.
I did not get what failed with this approach on your side, but here is a bullet-proof version:
method_declaration = %Q{
def %{method}
#_memoized_results ||= {}
if #_memoized_results.include? :%{method}
#_memoized_results[:%{method}]
else
#_memoized_results[:%{method}] = super()
end
end
}
methods.each do |method|
class_eval method_declaration % {method: method}
end