The service provided by Consileon was professional and comprehensive with a very good understanding of our needs and constrains.

Wolfgang Hafenmayer, Managing partner, LGT Venture Philanthropy

Technical quality of staff offered, capability of performing various project roles, as well as motivation and dedication to the project (... [...]

dr Walter Benzing, Head of development B2O, net mobile AG

Technical quality of staff offered, capability of performing roles of developers, lead developers, trainers, team leaders, architects as wel [...]

Karl Lohmann, Itellium Systems & Services GmbH

Firma Consileon Polska jest niezawodnym i godnym zaufania partnerem w biznesie, realizującym usługi z należytą starannością (...)

Waldemar Ściesiek, Dyrektor zarządzający IT, Polski Bank

The team was always highly motivated and professional in every aspect to perform on critical needs of our startup environment.

Denis Benic, Founder of Ink Labs

Better ruby defaults for hash-based options

Category: Other Tags:

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.


Tomasz Wójcik

Software developer, IT consultant.

A software programmer, master of Java who is not afraid to explore the Ruby on Rails. In the past he developed games in C, Objective-C and ActionScript 3 in these days he loves to do that after hours. Great fan of Git.


Tags:

Comments

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

Trwa ładowanie