Given the following program, in which I want to:
Struct
with some keysmodule Magic
def self.MagicStruct(keys_array, &block)
Struct.new(*keys_array) do
@keys = keys_array
def self.magic_class_method
puts "constructed with #{@keys}"
end
def magic_instance_method
puts "instance method"
end
# --- DOESN'T WORK, CONTEXT IS OUTSIDE OF MODULE --- #
# yield if block_given?
instance_eval(&block) if block_given?
end
end
end
Foo = Magic.MagicStruct([:a, :b, :c]) do
puts "class customizations executing..."
def self.custom_class_method
puts "custom class method"
end
def custom_instance_method
puts "custom instance method"
end
end
Foo.magic_class_method # works
Foo.custom_class_method # works
x = Foo.new({a: 10, b: 20, c: 30})
x.magic_instance_method # works
x.custom_instance_method # fails
Output:
class customizations executing...
constructed with [:a, :b, :c]
custom class method
instance method
Traceback (most recent call last):
`<main>': undefined method `custom_instance_method' for #<struct Foo a={:a=>10, :b=>20, :c=>30}, b=nil, c=nil> (NoMethodError)
Why is the self.custom_class_method
correctly added to the Foo
class, but the custom_instance_method
is not? This usage is clearly stated in the Struct documentation, so I'm afraid there's some kind of scoping or context issue I'm missing here.
I would prefer to keep the nice def method() ... end
syntax rather than resorting to having a strict requirement to use define_method("method")
in the customization block, which does happen to work.
In Ruby there is the notion of a current class which is the target of keywords such as def
.
When you use instance_eval
, the current class is set to self.singleton_class
. In other words, def x
and def self.x
are equivalent. In your code, custom_instance_method
is defined on the singleton class of the newly created Struct
, making it a class method.
When you use class_eval
, the current class is set to self
. Since this method is only available on classes, it will set the current class to the one you called the method on. In other words, def x
will define a method that's available to all objects of that class. This is what you wanted.
For more details, see my other answer.