There are many different ways to approach this. I recommend using an instance of the class that you define instead of Hash. For example, instead of ...
# Example of slow code using regular Hash. h = Hash.new h[:foo] = some_long_computation h[:bar] = another_long_computation # Access value. puts h[:foo]
... create your own class and define methods, for example ...
class Config def foo some_long_computation end def bar another_long_computation end end config = Config.new puts config.foo
If you need an easy way to cache long calculations, or it should absolutely be a hash, not your own class, now you can wrap a Config instance with Hash.
config = Config.new h = Hash.new {|h,k| h[k] = config.send(k) } # Access foo. puts h[:foo] puts h[:foo] # Not computed again. Cached from previous access.
One problem with the above example is that h.keys will not include :bar because you have not yet accessed it. Thus, you could not, for example, iterate over all keys or entries in h , because they do not exist until they are available. Another potential problem is that your keys must be valid Ruby identifiers, so arbitrary String keys with spaces will not work when defining them on Config .
If it matters to you, there are different ways to deal with it. One way to do this is to populate your thunks hash and force thunks to be used on access.
class HashWithThunkValues < Hash def [](key) val = super if val.respond_to?(:call) # Force the thunk to get actual value. val = val.call # Cache the actual value so we never run long computation again. self[key] = val end val end end h = HashWithThunkValues.new # Populate hash. h[:foo] = ->{ some_long_computation } h[:bar] = ->{ another_long_computation } h["invalid Ruby name"] = ->{ a_third_computation } # Some key that an invalid ruby identifier. # Access hash. puts h[:foo] puts h[:foo] # Not computed again. Cached from previous access. puts h.keys #=> [:foo, :bar, "invalid Ruby name"]
One caveat with this last example is that it will not work if your values ββare callable, because it cannot distinguish between a ton that must be forced and a value.
Again, there are ways to handle this. One way to do this is to save a flag that marks whether the value has been evaluated. But for each record, additional memory is required. A better way would be to define a new class to note that the Hash value is an invaluable thunk.
class Unevaluated < Proc end class HashWithThunkValues < Hash def [](key) val = super
The disadvantage of this is that now you have to remember that you use Unevaluted.new when filling in your hash. If you want all values ββto be lazy, you can also override []= . I don't think this actually saves a lot of input, because you still have to use Proc.new , proc , lambda or ->{} to create the block in the first place. But it can be helpful. If you did, it might look something like this.
class HashWithThunkValues < Hash def []=(key, val) super(key, val.respond_to?(:call) ? Unevaluated.new(&val) : val) end end
So here is the complete code.
class HashWithThunkValues < Hash