ordered hashes in ruby 1.8 19. Jun 2008

Ruby 1.8 doesn’t offer builtin ordered hashes, but there are several libraries out there which extend or replace Ruby’s Hash class. Among them are Ruby/RBTree, Ara T. Howard’s orderedhash and Ruby Facets. All three of these offer a hashlike syntax, letting you do things like

ordered_hash = Library['a', 1, 'b' ,2]

resulting in

{'a'=>1, 'b'=>2}

Sadly all three of these libraries have poor documentation. The one with most bearable in my eyes is Ruby Facet’s Dictionary, which also has a RubyGem available, so let’s have a look at it.

$ sudo gem install facets
Successfully installed facets-2.4.1
1 gem installed
Installing ri documentation for facets-2.4.1...
Installing RDoc documentation for facets-2.4.1...

Ruby Facets is a rather large library, so you’ll want to require just the Dictionary class.

>> require 'rubygems'
>> require 'facets/dictionary'

Let’s make an ordered hash with five elements and print it out.

>> dictionary = Dictionary[*(1..10)]
>> dictionary.each {|k,v| puts "#{k}=>#{v}"}
1=>2
3=>4
5=>6
7=>8
9=>10

Works fine. Now for the bonus round. Dictionary can also be used as a sorted hash by calling the order_by or more conveniently the order_by_key method. Once called this method ensures that all previously and subsequently inserted keys will be in sorting order.

>> array = Array.new(10){rand(100)}
>> dictionary = Dictionary[*array]
=> {1=>49, 48=>97, 17=>66, 6=>70, 52=>77}
>> dictionary.order_by_key
>> dictionary.each {|k,v| puts "#{k}=>#{v}"}
1=>49
6=>70
17=>66
48=>97
52=>77
 

ordered hashes in ruby 1.9 19. Jun 2008

If you create a hash in Ruby 1.8 and then browse it with the each-iterator you get the keys delivered in abitrary order.

>> hash = {:a=>1, :b=>2, :c=>3,}
>> hash.each {|key,value| puts "#{key}=>#{value}"}
c=>3
b=>2
a=>1

This might be okay for some cases, but in other cases you might want the insertation order preserved. Well, good news in Ruby 1.9 this works.

>> hash = {:a=>1, :b=>2, :c=>3,}
>> hash.each {|key,value| puts "#{key}=>#{value}"}
a=>1
b=>2
c=>3
 

slow iterators in ruby 1.9 on mac 17. Jun 2008

If you ever wondered why your Ruby programs are not faster but much slower with Ruby 1.9 you might be using each or times iterators on large collections.

10000000.times {}

In fact if you do a benchmark,

require 'benchmark'

n = 10000000

Benchmark.bm do |x|
  x.report { n.times {} }
end

you get stunning results.

$ ruby bench.rb
   user     system      total        real
   0.720000   0.000000   0.720000 (  0.721872)
$ ruby1.9 bench.rb 
   user     system      total        real
   16.670000   9.110000  25.780000 ( 25.923977)

When increasing the number of iterations by powers of ten it looks like the computational complexity of the times iterator is O(n) in Ruby 1.9. According to Antonio Cangiano this behaviour is specific to Mac OS X and does not appear on Linux.

 

nested hashes with default value 17. Jun 2008

To instantiate arbitrarily nested hashes in Ruby you can write

h = Hash.new{|hash,key| hash[key] = Hash.new(&h.default_proc)}

This let’s you do

h[1] = 2 # => 2
h[2][1] = 3 # => 3
h[3][1][1] = 4 # => 4
h.inspect # => {1=>2, 2=>{1=>3}, 3=>{1=>{1=>4}}}

That’s nice, but maybe you want your innermost hash to have a default value to access the keys via +=-operator without having to check if the key has been initialized before? You might be tempted to write the following.

h = Hash.new(Hash.new(0))

Looks nice. Now let’s assign a value and check if it’s stored correctly.

h[1][1] = 2 # => 2
h[2][2] = 3 # => 3

Looks fine too. Now let’s inspect the hash.

h.inspect # => {}

Whoops! Where have my keys gone? Let’s examine the inner hashes.

h[1].object_id # => 1751410
h[2].object_id # => 1751410

Okay, so obviously instead of instantiating new hashes for each key the same hash was used. Let’s examine the default value.

h.default.object_id # => 1751410

Ah, so the keys were stored in the default value hash. Let’s check and see.

h.default # => {1=>2, 2=>3}

So instead of generating a nested structure for each key all keys are stored flatly in the innermost hash. That is not what we wanted. We can however talk the hash into doing the intented by using a default procedure instead of a default value, which is of course documentated in the lovely core API documentation.

h = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = 0}}

While working fine in Ruby 1.9, this causes problems in in Ruby 1.8 because block arguments are not local there.

h = Hash.new {|h,k| h[k] = Hash.new {|h,k| h[k] = 0}} # => {}
h[1][1] = 1 # => 1
h.inspect # {1=>{1=>1}}
h[2][1] # => 0
h.inspect # => {1=>0}

To get it working in Ruby 1.8 the variables in each block have to be uniquely named.

h = Hash.new {|h,k| h[k] = Hash.new {|h1,k1| h1[k1] = 0}} # => {}
h[1][1] = 1 # => 1
h.inspect # {1=>{1=>1}}
h[2][1] # => 0
h.inspect # {1=>{1=>1}, 2=>{1=>0}}

Now we can fill the twodimensional hash with data and see the result.

h = Hash.new {|h,k| h[k] = Hash.new {|h1,k1| h1[k1] = 0}} # => {}
h[1][1] += 1 # => 1
h[1][2] += 2 # => 2
h[2][3] += 3 # => 3
h.inspect # => "{1=>{1=>1, 2=>2}, 2=>{3=>3}}"
h[1][3] # => 0
h.inspect # => {1=>{1=>3, 2=>4, 3=>0}, 2=>{1=>0, 3=>6}}

Et voilà!

 

installing ruby 1.9 on leopard 17. Jun 2008

Thanks to Caspar Florian Ebeling installing Ruby 1.9 on Mac OS 10.5 is now a breeze. Just install MacPorts if you haven’t already and then issue a

sudo port install ruby19

Now you can launch Ruby 1.9 with ruby1.9 and irb with irb1.9.

For the more adventurous souls among you here’s how to compile it by hand. To compile Ruby 1.9 you need Readline 5.2, therefore it will be installed if it isn’t already there. Commands with a trailing backslash are multiline statements. If you prefer to compile Readline 5.2 on Leopard by hand, you should have a look at this patch.

sudo port install readline
curl -O ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.0-0.tar.gz
tar xvfz ruby-1.9.0-0.tar.gz
cd ruby-1.9.0-0
./configure --prefix=`echo ~`/ruby19 --program-suffix=1.9 \
--with-readline-dir=/opt/local
make
make install

Now add ~/ruby19/bin to your PATH-variable (for example in ~/.bash_profile or ~/.bashrc)

export PATH=~/ruby19/bin:$PATH

To activate the changes just launch a new terminal window. Now you can launch Ruby 1.9 with ruby1.9 and irb with irb1.9.

 

1 2 3