Better ruby defaults for hash-based options
2014-06-17 · ruby
Almost every ruby, Ruby on Rails project has some kind of a global configuration. Sometimes it’s a YAML file loaded ‘as-is’, other times it’s a model or designated configuration class.
There are cases when we have to fallback to default values. In a model or
configuration class the easiest way is to use accessor with the or
operator:
class Config
def initialize
@hash = ... # empty hash or loaded YAML file
end
def option=(value)
@hash['option'] = value
end
def option
@hash['option'] || "option's default value"
end
end
However this approach have a major drawback: what if we wanted a nil
as the
option value?
config.option = nil # => nil
config.option # => "option's default value"
We get the same problem for false
value:
config.option = false # => false
config.option # => "option's default value"
Hash#fetch
to the rescue
There is an elegant solution to the nil
values: Hash#fetch
.
This method returns the key value or throws an error (KeyError
) if they key
wasn’t found.
def option
@hash.fetch('option')
end
config.option # => KeyError: key not found: "option"
config.option = nil # => nil
config.option # => nil
We have several ways for handling default values:
- method with two params:
Hash#fetch(key, default)
- method with a param and a block:
Hash#fetch(key) { default }
- method with a param and a
Proc
as param:Hash#fetch(key, &block)
Implementing option accessor with any of the mentioned methods gives us
possibility to use nil
and false
as option values.
config.option # => "option's default value"
config.option = nil # => nil
config.option # => nil
The Pitfall
There’s a little niuance in providing default values via #fetch
most coders
aren’t aware of: When the default value is evaluated?
In the two params version (the one without block param) both parameters are ALWAYS evaluated. That’ right: even if there is a value provided, the default value will be evaluated.
def default
puts 'default evaluated!'
'default value'
end
def option
@hash.fetch('option', default)
end
config.option # prints "default evaluated!", returns "default value"
config.option = 'set option'
config.option # prints "default evaluated!", returns "set option"
Thankfully the block version evaluates block only if there is no value.
In this simple example evaluating default value isn’t a big thing. However imagine a situation when you perform time-consuming operation like searching through the huge database or retrieving OAuth access token from the server.
def retrieve_oauth2_access_token
... # time consuming operation that sends a request for access token
end
def access_token
@hash.fetch('access_token', retrieve_oauth2_access_token)
end
Now every call to your config’s #access_token
method will send a request to
the server even if the token was obtained on the first call. For the sake of
time and good practices you don’t want to send a request to remote machine
every time you want to use a token. Good practice is to pass a lambda
as 2nd
parameter instead of defining a block. That will save you time when you have
the same default value in the many places.
def retrieve_oauth2_access_token
... # time consuming operation that sends a request for access token
end
DEFAULT_OAUTH2 = -> { retrieve_oauth2_access_token }
def access_token
@hash.fetch('access_token', &DEFAULT_OAUTH2) # still a block version
end
Being aware of this issue can save you lots of time trying to debug the performance issues in your application.
Conclusion
When it comes to manage configuration it’s almost always better to use the
#fetch
method over the or
operator. Not only because it allows nil
values.
It also helps you discovering missing configuration parts and handle missing
keys with ease.
Autor: Tomasz Wójcik

Programista, konsultant IT. W pracy zajmuje się głównie Javą, skrupulatnie zgłębia tajniki Rubyego na szynach. Kiedyś programował gry w C, Objective-C i ActionScript 3 (oraz sterowniki w asemblerze), teraz robi to wyłącznie po pracy. Wielki zwolennik gita.