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?
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