Thursday, March 26, 2009

Constant Hashes w/defaults in Ruby

Hi there. I'm back after a long hiatus. Since my last post, I've moved across the country from Buffalo, NY to Berkeley, CA, had 2 jobs, and become a father. So I ask your forgiveness for the long period between posts.

On to some code. I like to use Constants in Ruby whenever feasible. It's a logical and readable-to-the-next-coder way to store information in a way that indicates it should not change. It also gets some compile-time optimization for speed, and takes up less memory as a datum shared across the various instances of a class.

I'm particularly fond of constant Hashes for small lookup tables. Let's imagine some generic Rails controller with this code snippet:

BANAL_MESSAGE_FOR = {
:edit => 'You are editing.',
:show => 'You are viewing a single instance.'
}


Hashes in Ruby have a handy method called default= that sets what the default looked-up value should be when the key for the lookup is not found in the Hash. (You can think of the default default as nil, if that makes any sense.) However, default= returns the default value, rather than the new Hash. So this gets problematic:


BANAL_MESSAGE_FOR = {
:edit => 'You are editing.',
:show => 'You are viewing a single instance.'
}.default = "I don't know what you're doing".


The example above will break, as BANAL_MESSAGE_FOR is no longer a Hash at all.


BANAL_MESSAGE_FOR = {
:edit => 'You are editing.',
:show => 'You are viewing a single instance.'
}
BANAL_MESSAGE_FOR.default = "I don't know what you're doing".


The example above will lead to compile-time warnings about modifying a Constant.

What alternatives exists for this issue?



I've grown fond of the following approach:

BANAL_MESSAGE_FOR = lambda do
message_for = {
:edit => 'You are editing.',
:show => 'You are viewing a single instance.'
}
message_for.default = "I don't know what you're doing".
message_for
end.call


This allows us to define the particular cases with explicit Hash pairs, set a default value, and keep everything in Constant world. Depending on the particulars, we could instead go with either a class variable (@@banal_message_for) or a class method.

I still like constant-from-a-lambda, though - even despite the somewhat off-putting complexity of the lambda/call syntax. It makes it clear that we're dealing with constant data, and the complexity from the call syntax seems less egregious to me than having an additional method whose only purpose is to be called once at app start.

What do you think?

3 comments:

Pieter said...

In Ruby 1.8.6 (c/o tests on my machine):

CONSTANT = { :a => 'a', :b => 'b' }
CONSTANT.default = 'c'

throws no warnings or errors.

Given some of the limitations to the #default= method on a Hash, I tend to prefer this form:

CONSTANT = Hash.new('c') # or a block!
CONSTANT[:a] = 'a'
CONSTANT[:b] = 'b'

Kevin C. Baird said...

Thanks, Pieter. Both good approaches.

Another alternative that occurred to me later was

CONSTANT =
Hash.new(:the_default).merge(
HASH_WITH_OTHER_PAIRS
)

aizatto said...

If you are using Rails v2.3.2, or the andand ruby gem or Ruby 1.9 you can do:

CONSTANT = { :a => 'a', :b => 'b' }.tap { |c| c.default = 'c' }