Search code examples
rubystructautovivification

How to implement autovivification for Ruby structs?


Ruby has support for autovivification for hashes by passing a block to Hash.new:

hash = Hash.new { |h, k| h[k] = 42 }
hash[:foo] += 1   # => 43

I'd like to implement autovivification for structs, also. This is the best I can come up with:

Foo = Struct.new(:bar) do
  def bar
    self[:bar] ||= 42
  end
end

foo = Foo.new
foo.bar += 1   # => 43

and of course, this only autovivifies the named accessor (foo.bar), not the [] form (foo[:bar]). Is there a better way to implement autovivification for structs, in particular one that works robustly for both the foo.bar and foo[:bar] forms?


Solution

  • I would go with the following approach:

    module StructVivificator
      def self.prepended(base)
        base.send(:define_method, :default_proc) do |&λ|
          instance_variable_set(:@λ, λ)
        end
      end
      def [](name)
        super || @λ && @λ.() # or more sophisticated checks
      end
    end
    
    Foo = Struct.new(:bar) do
      prepend StructVivificator
    end
    
    foo = Foo.new
    foo.default_proc { 42 } # declare a `default_proc` as in Hash
    
    foo[:bar] += 1   # => 43
    foo.bar += 1     # => 44
    

    foo.bar above calls foo[:bar] under the hood through method_missing magic, so the only thing to overwrite is a Struct#[] method.

    Prepending a module makes it more robust, per-instance and in general more flexible.


    The code above is just an example. To copy the behavior of Hash#default_proc one might (credits to @Stefan for comments):

    module StructVivificator
      def self.prepended(base)
        raise 'Sorry, structs only!' unless base < Struct
    
        base.singleton_class.prepend(Module.new do
          def new(*args, &λ) # override `new` to accept block
            super(*args).tap { @λ = λ }
          end
        end)
        base.send(:define_method, :default_proc=) { |λ| @λ = λ }
        base.send(:define_method, :default_proc) { |&λ| λ ? @λ = λ : @λ }
    
        # override accessors (additional advantage: performance/clarity)
        base.members.each do |m|
          base.send(:define_method, m) { self[m] }
          base.send(:define_method, "#{m}=") { |value| self[m] = value }
        end
      end
      def [](name)
        super || default_proc && default_proc.(name) # or more sophisticated checks
      end
    end
    

    Now default_proc lambda will receive a name to decide how to behave in such a case.

    Foo = Struct.new(:bar, :baz) do
      prepend StructVivificator
    end
    
    foo = Foo.new
    foo.default_proc = ->(name) { name == :bar ? 42 : 0 }
    puts foo.bar          # => 42
    puts foo[:bar] += 1   # => 43
    puts foo.bar += 1     # => 44
    puts foo[:baz] += 1   # => 1