Reputation: 5436
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.
Upvotes: 1
Views: 337
Reputation: 3182
This is a gem I wrote that I believe does most, if not all that you want (not so sure about adding class methods though, I'd have to play with that). While the Struct
created is immutable, you can have a look at the code and alter it to meet your needs; it's pretty simple. What you'd be interested in, would be here, which basically amounts to what you see below. The extension of the modules uses instance_eval
in a way that I believe is what you want as well:
# https://github.com/gangelo/immutable_struct_ex/blob/main/lib/immutable_struct_ex.rb
# Defines the methods used to create/manage the ImmutableStructEx struct.
module ImmutableStructEx
class << self
# Most likely changing this method name to #create in subsequent version, but alas...
def new(**hash, &block)
Struct.new(*hash.keys, keyword_init: true, &block).tap do |struct|
return struct.new(**hash).tap do |struct_object|
struct_object.extend Comparable
struct_object.extend Immutable
end
end
end
end
end
Usage details can be found in the README.md in the github repository.
I hope this help, at least in part.
Upvotes: 1
Reputation: 17020
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.
Upvotes: 2