November 13, 2015 by Daniel P. Clark

Implement a Lazy Hash in Ruby

I use the term Lazy Hash in the same way you would call an Enumerator a lazy iterator.  That being; the values don’t get evaluated until they are needed.  In a Hash this is also utilizing memoization, where the value gets assigned the first time it’s used.  E.G. @variable ||= :value

Let’s say you own a used bookstore and your computer database of books is implemented in a Ruby Hash store.  There is no way your store will have every book in existence cataloged in the Hash, so you want to store only the books info you need as you need them.

When you create a Hash it has a few implementations for returning a default value for non-existent entries.  By default the return value is nil.  But you can also set either a static return value with the :default= method, or a proc with :default_proc=.  You can only have one of these working at a time, and whichever you set last will be the default.  Here we will be taking advantage of :default_proc=

The Hash#default_proc will be passed two parameters; the hash itself, and the queried key. Hash.new {|hash,key| … }

For your bookstore lets say you can lookup any books info online through an API query with an ISBN number.  This service costs you money so you don’t want to pass the rate limits and pay more than you have to.  So you build a Lazy Hash.  Your code will look as follows.

books = {}
books.default_proc = {|hash, isbn|
  hash[isbn] = OnlineISBNDataBase.query(isbn)
}

Now anytime you access the books Hash, if the book ISBN is already in the Hash you get that result.  If it’s not in your Hash then your default_proc is called and the paid service API lookup is performed.  This then stores the result in your Hash so the next time you look it up you have a local copy saved and you don’t have to pay for the same request twice.  The default_proc only gets called if the key does not exist in the Hash.

That pretty much sums up the Lazy Hash.  You are not limited to just one action in a proc, you can do whatever you want.  The following two examples are not Lazy Hashes.  They are just examples showing you could do much more with a Hash.

blacklisted_books = ["The Catcher in the Rye"]

library = {
  "Lord of the Rings" => {isle: 5, column: 2, row: 3},
  "Enders Game" => {isle: 2, column: 1, row: 4}
}

library.default_proc = proc {|h,i|
  if blacklisted_books.grep /#{i}/i
    puts "NotifySecretGovermentAgency.alert_queried_blacklist(\"#{i}\")"
    "Book on loan"
  else
    "No book available by that name"
  end
}

library["Lord of the Rings"]
# => {:isle=>5, :column=>2, :row=>3} 

library["Catcher in the Rye"]
#NotifySecretGovermentAgency.alert_queried_blacklist("Catcher in the Rye")
# => "Book on loan"

Or even partial title lookup.

library = {
  "Lord of the Rings" => {isle: 5, column: 2, row: 3},
  "Lord of the Flies" => {isle: 8, column: 3, row: 2},
  "Enders Game" => {isle: 2, column: 1, row: 4}
}

library.default_proc = proc {|h,i|
  # Find close match
  matches = h.keys.grep(/#{i}/i)
  if matches.any?
    matches.zip(matches.map(&h.method(:[])))
  else
    "No book available by that name"
  end
}

library["lord of the"]
# => [["Lord of the Rings", {:isle=>5, :column=>2, :row=>3}], ["Lord of the Flies", {:isle=>8, :column=>3, :row=>2}]]

Collection actions

One of the great features in Ruby is the ability to perform actions over an Array, or Hash, of things with both ease and simplicity.  You can use a bloc or proc for these actions.  Example:

[1,2,3,4,5].map {|number| number.to_s }
# => ["1", "2", "3", "4", "5"]

stringify = proc {|number| number.to_s }
[1,2,3,4,5].map(&stringify)
# => ["1", "2", "3", "4", "5"]

[1,2,3,4,5].map(&:to_s)
# => ["1", "2", "3", "4", "5"]

The last example shows calling the to_s method on each item.  For more info on how that works see my blog post: When to use Ampersand and/or Colon with Proc, Map, and Inject

Hash#to_proc

In the next coming release of Ruby, version 2.3.0, we have the to_proc method on the Hash object [Feature #11563].

h = { a: 1, b: 2, c: 3, d: 4 }
[ :a, :c, :d, :b ].map(&h)
# => [ 1, 3, 4, 2 ]

Here where you see the &h the & (at) symbol calls the to_proc method on the Hash object.  This takes one value per proc call and with map it simply iterates over each item, calls the proc with that value, and maps the return value to that place.  If you’d like to use this feature before Ruby 2.3 you can do &your_hash.method(:[]) .  You can see that in my partial book search example above matches.zip(matches.map(&h.method(:[]))) .  But in Ruby 2.3 this can be written more cleanly as matches.zip(matches.map(&h)) .

Note that a proc has a shortcut method to call it [ ] .  It looks just like a Hash.

the_proc = proc {|val| val.class }
the_proc.call(1)
# => Fixnum
the_proc[1]
# => Fixnum

the_hash = {}
the_hash.default_proc = proc {|hash, val| val.class }
the_hash[1]
# => Fixnum

proc_hash = the_hash.method(:[])
proc_hash.class
# => Method
proc_hash.call(1)
# => Fixnum
proc_hash[1]
# => Fixnum

# Ruby 2.3 Hash#to_proc
proc_hash = the_hash.to_proc
proc_hash.class
# => Proc
proc_hash.call(1)
# => Fixnum
proc_hash[1]
# => Fixnum

So you can give a key to a Proc hash in exactly the same way you’d give a key to the Hash itself.

Procs have a great future in Ruby.  They are extremely useful as is, but we may get even more powerful functionality as composition [Feature #6284] is being discussed as a feature to add to procs.  If you haven’t used curry yet, give it a try. 🙂

Summary

Hashes are far more powerful than a standard key -> value lookup store.  You can get very dynamic with your implementation and have some fun.

dmv_wating_list.default_proc = proc {|hash, name| hash[hash.count] = name}

Pick a number and wait in line 😉 ( Note above they don’t call your name, they call your number.  You only give your name once 😉 )

Often you are only limited by your own self.  I suppose that is a reason why we seek out greater knowledge to overcome our own limitations.  Wow!  That got philosophical rather quickly.

Anyways I was exited that my proposal of adding Hash#to_proc to the Ruby language was accepted and this has lead me to go into more robust use cases for Hashes since this blog couldn’t think of a purpose for it.  Hey, it’s beautiful, short-handed, and makes programmers happy.  That’s reason enough ^_^.

As always I hope you’ve enjoyed this!  Please comment, share, subscribe to my RSS Feed, and follow me on twitter @6ftdan!

God Bless!
-Daniel P. Clark

Image by Marion hobbs via the Creative Commons Attribution 2.0 Generic License

#default_proc#hash#lazy hash#proc#ruby

Comments

  1. Nafaa
    November 14, 2015 - 6:09 am

    Great thanks.

Leave a Reply

Your email address will not be published / Required fields are marked *