Dynamically Defined Setter Methods Using Define_Method

Dynamically defined setter methods using define_method?

Here's a fairly full example of using define_method in a module that you use to extend your class:

module VerboseSetter
def make_verbose_setter(*names)
names.each do |name|
define_method("#{name}=") do |val|
puts "@#{name} was set to #{val}"
instance_variable_set("@#{name}", val)
end
end
end
end

class Foo
extend VerboseSetter

make_verbose_setter :bar, :quux
end

f = Foo.new
f.bar = 5
f.quux = 10

Output:


@bar was set to 5
@quux was set to 10

You were close, but you don't want to include the argument of the method inside the arguments of your call to define_method. The arguments go in the block you pass to define_method.

define_method: How to dynamically create methods with arguments

If I understand your question correctly, you want something like this:

class Product
class << self
[:name, :brand].each do |attribute|
define_method :"find_by_#{attribute}" do |value|
all.find {|prod| prod.public_send(attribute) == value }
end
end
end
end

(I'm assuming that the all method returns an Enumerable.)

The above is more-or-less equivalent to defining two class methods like this:

class Product
def self.find_by_name(value)
all.find {|prod| prod.name == value }
end

def self.find_by_brand(value)
all.find {|prod| prod.brand == value }
end
end

Using define_method and meta-programming to define instance methods dynamically in Ruby?

In your original code, you defined all the methods to take no arguments:

def education
# ^^^
@education ||= Education.new(self)
end

In the metaprogrammed code, you define all the methods to take a single argument called argument:

define_method(m) do |argument|
# ^^^^^^^^^^
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end

However, you call it with zero arguments:

puts Country.new.education.inspect
# ^^^

Obviously, your methods are meant to be lazy getters, so they should take no arguments:

define_method(m) do
instance_variable_set("@#{m}", Object.const_get(m.capitalize).new(self))
end

Note that there are other problems with your code. In your original code, you use a conditional assignment to only perform the assignment if the instance variable is undefined, nil or false, whereas in the metaprogrammed code, you are always unconditionally setting it. It should be something more like this:

define_method(m) do
if instance_variable_defined?(:"@#{m}")
instance_variable_get(:"@#{m}")
else
instance_variable_set(:"@#{m}", const_get(m.capitalize).new(self))
end
end

Note: I also removed the Object. from the call to const_get to look up the constant using the normal constant lookup rules (i.e. first lexically outwards then upwards in the inheritance hierarchy), since this corresponds to how you look up the constants in the original code snippet.

This is not fully equivalent to your code, since it sets the instance variable only when it is undefined and not also when it is false or nil, but I guess that is closer to your intentions anyway.

I would encapsulate this code to make its intentions clearer:

class Module
def lazy_attr_reader(name, default=(no_default = true), &block)
define_method(name) do
if instance_variable_defined?(:"@#{name}")
instance_variable_get(:"@#{name}")
else
instance_variable_set(:"@#{name}",
if no_default then block.(name) else default end)
end
end
end
end

class Country
attr_reader :name

COMPONENTS = %w(government symbols economy education healthcare holidays religion)

COMPONENTS.each do |m|
lazy_attr_reader(m) do |name|
const_get(name.capitalize).new(self))
end
end

def initialize
@name = 'MyName'.freeze
end
end

That way, someone reading your Country class won't go "Huh, so there is this loop which defines methods which sometimes get and sometimes set instance variables", but instead think "Ah, this is a loop which creates lazy getters!"

define_method for setter wont work inside class call

module M
def create(name)
define_method("#{name}=") do |value|
"called #{name} with #{value}"
end
end
end

class C
extend M
create(:custom)
def initialize(val)
puts public_send(:custom=, val) # this is the only change needed
end
end

C.new('haha')
# called custom with haha

I only had to change one line in your code.

There were two problems with your code:

  1. custom = val is not a method call, it assigns to a local variable named custom. If you want to call a setter, you need to make it explicit that you are calling a method, by providing an explicit receiver: self.custom = val. See Why do Ruby setters need “self.” qualification within the class?
  2. Assignments evaluate to the right-hand side of the assignment. The return value of a setter method is ignored, unless you don't use assignment syntax, i.e. public_send. See Why does irb echo the right hand side of an assignment instead of the return value in the case of a setter method?

Why are these methods not dynamically defined at runtime by define_method?

define_method doesn't define class methods, it defines instance methods. You want define_singleton_method to define class methods in the code you have above.

How to adapt this code to use getter/setter?

This was the solution:

    private void CreateProperty(TypeBuilder typeBuilder, string propertyName, Type propertyType)
{
PropertyBuilder propertyBuilder = typeBuilder.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
MethodBuilder getPropMthdBldr = typeBuilder.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
ILGenerator getIl = getPropMthdBldr.GetILGenerator();

string methodName = "ReadProperty" + propertyType.Name;

getIl.Emit(OpCodes.Ldarg_0);
getIl.Emit(OpCodes.Ldstr, propertyName);
getIl.Emit(OpCodes.Call, typeof(MyClassParent).GetMethod("ReadProperty"));
getIl.Emit(OpCodes.Ret);

propertyBuilder.SetGetMethod(getPropMthdBldr);
}


Related Topics



Leave a reply



Submit