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
Find Memory Leak in a Ruby on Rails Project
Using Ruby, Reading a File, Containing Name/Value Pairs into a Hash
How to Load Some Activerecord Models from a Yaml File and Save Them to the Db
How to Find a Model's Relationships
Polymorphic Association with Multiple Associations on the Same Model
Heroku: How to Push Seeds.Rb to Existing Rails App
Paperclip Amazon S3 Setup with Heroku
Vi Input Mode in Command Line Matlab
If Else Statements in .Html.Erb in Views
Rails: How to Get a File Extension/Postfix Based on the Mime Type
Are There Better Ways to Prevent 'Yield' When No Block Is Passed In
How to Instruct Capistrano 3 to Load My Shell Environment Variables Set at Remote Host
Rails Before_Filter for Specific Actions in Controller
What Happens When Modifying Gemfile.Lock Directly
Verb-Agnostic Matching in Sinatra
How to Tokenize This String in Ruby