How to implement autovivification for Ruby structs?

137 Views Asked by At

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?

1

There are 1 best solutions below

5
Aleksei Matiushkin On BEST ANSWER

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