371273
371273

Reputation: 5436

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.

Upvotes: 1

Views: 337

Answers (2)

gangelo
gangelo

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

Matheus Moreira
Matheus Moreira

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

Related Questions