loading...

Struct With Hash-like Default Proc

mudasobwa profile image Aleksei Matiushkin Originally published at rocket-science.ru on ・2 min read

This question was originally asked on StackOverflow, and the answer is more-or-less trivial; in any case I find myself explaining some of this metaprogramming techniques as often, as I decided to finally write this short post on the topic.

Q. How to implement autovivification for Ruby structs?

Everybody programming ruby at least three months should have met theHash#default_procbehaviour. This default_proc might be also passed directly to the constructor of the hash:

 hash = Hash.new { |h, k| h[k] = 42 }
#⇒ {}
 hash[:answer]
#⇒ 42

One might require the sane functionality from structs (why not, after all?) Indeed, this is easy.

Let’s start with the complete working example.

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

############################################
##### usage example
############################################

Foo = Struct.new(:bar, :baz) do
  prepend StructVivificator
end

Here we declared the module to prepend. Once prepended, it checks whether it was prepended to Struct, and declares two methods on the base class: getter and setter for default_proc. Also, it overwrites the default Struct#[]property getter, trying to call the superior method and gracefully falling back to the call to default_proc, if declared.

Since the .property access calls [:property] under the hood through method_missing magic, the only thing to overwrite is a Struct#[] method.

So far so good. Let’s test it.

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

Posted on Dec 25 '18 by:

mudasobwa profile

Aleksei Matiushkin

@mudasobwa

NB! I am _not_ a member of #DEVCommunity. → I like: Elixir, Erlang, Ruby, R, C, COBOL. → I hate: Apple, JS, Rails, haters. → I am more functional, than object oriented.

Discussion

markdown guide