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

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.