Define_Method: How to Dynamically Create Methods with Arguments

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

How to dynamically create methods with parameter in ruby?

You should use public_send to call methods based on their name:

  ['id', 'message', 'votes_count'].each do |method|
define_method "#{method}" do |parameter|
parameter.public_send(method)
end
end

Dynamically set parameters in define_method

So as advised I went to class_eval.

class Whatever
class << self
def add_method(name, parameters = {})
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{name}(#{method_parameters(parameters)})
#{method_body(parameters)}
end
RUBY
end

# method_parameters({
# foo: { required: false, default: 0 },
# bar: { required: true }
# })
# => "foo: 0, bar:"
def method_parameters(parameters)
parameters.map do |key, options|
value = options[:required] ? '' : " #{options[:default] || 'nil'}"
"#{key}:#{value}"
end.join(', ')
end

# method_parameters({
# foo: { required: false, default: 0 },
# bar: { required: true }
# })
# => "[foo, bar]"
def method_body(parameters)
"[#{parameters.keys.map(&:to_s).join(', ')}]"
end
end
end
params = {
foo: { required: false, default: 0 },
bar: { required: true }
}

Whatever.add_method(:hello, params)

Whatever.new.hello(bar: true) # => [0, true]
Whatever.new.hello(foo: 42, bar: true) # => [42, true]
Whatever.new.hello # missing keyword: bar (ArgumentError)

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 parameters for meta programming in Ruby

Try passing an array or dictionary.

UPDATE:

if condition1
class_eval <<-EVAL
def #{"my_method"}(arg1)
end
EVAL
else
class_eval <<-EVAL
def #{"my_method"}
end
EVAL
end

UPDATE2:

if condition1
self.instance_eval <<-EVAL
def #{"my_method"}(arg1)
end
EVAL
else
self.instance_eval <<-EVAL
def #{"my_method"}
end
EVAL
end

UPDATE3:

# or
self.instance_eval("def method1(arg1) puts 'hellowa' + arg1.to_s; end")
self.instance_eval("def method2() puts 'hellowa2'; end")

# and then
method1(33) # => hellowa33
method2 # => hellowa2

How do you pass arguments to define_method?

The block that you pass to define_method can include some parameters. That's how your defined method accepts arguments. When you define a method you're really just nicknaming the block and keeping a reference to it in the class. The parameters come with the block. So:

define_method(:say_hi) { |other| puts "Hi, " + other }

define_method with predefined keyword arguments

You have to use eval to define arguments dynamically (not just keyword arguments), e.g. using class_eval:

class MyClass
name = :foo
args = [:bar, :baz]
class_eval <<-METHOD, __FILE__, __LINE__ + 1
def #{name}(#{args.map { |a| "#{a}:" }.join(', ')}) # def foo(bar:, baz:)
[#{args.join(', ')}] # [bar, baz]
end # end
METHOD
end

MyClass.new.foo(bar: 1, baz: 2)
#=> [1, 2]

MyClass.instance_method(:foo).parameters
#=> [[:keyreq, :bar], [:keyreq, :baz]]

dynamically create methods in python

In Python, a function will lookup non-local names in the scope where it was defined or in the global scope if the name still does not exist there. If the value associated to the name changed, so will the returned value. Note that this is not specific to lambda functions.

A way around this is to create a closure by writing a helper function.

def fun(ret):
return ret

class A:
def __init__(self):
def apply_fun(item):
return lambda: fun(item)

for string in ['a', 'b']:
setattr(self, string, apply_fun(string))

print(A().a()) # 'a'

Alternative solution

In that particular case, using __getattr__ might be more suited as it is intended to dynamically return attributes.

def fun(ret):
return ret

class A:
def __getattr__(self, item):
if item in ['a', 'b']:
return lambda: fun(item)

print(A().a()) # 'a'


Related Topics



Leave a reply



Submit