Search code examples
rubymetaprogramming

Customizing a Ruby Struct with pre-defined definitions and a custom block


Given the following program, in which I want to:

  1. Create a Struct with some keys
  2. Provide some default customization
  3. Allow a block to be passed for further customization
module 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.


Solution

  • 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.