Peter Marklund

Peter Marklund's Home

Thu June 26, 2008
Programming

Ruby Gotcha: Default Values for the options Argument Hash

Like in Java, method arguments in Ruby are passed by reference rather than by copy. This means it's possible for the method to modify the arguments that it receives. This can happen unintentionally and be a very unpleasant surprise for the caller. A good example of this is the typical options = {} at the end of the method argument list. If you set a default value in that hash then that is a potential issue for the caller when the options hash is reused in subsequent calls (i.e. in a loop). See below for an example:

  def foreign_key(from_table, from_column, options = {})
    options[:to_table] ||= from_column.to_s[/^(.+)_id$/, 1].pluralize
    execute ["alter table #{from_table}",
            "add constraint #{foreign_key_name(from_table, from_column)}",
            "foreign key (#{from_column})",
            "references #{options[:to_table]}(id)",
            "#{on_delete_clause(options[:on_delete])}"].join(" ")
  end
  
  def foreign_keys(from_table, from_columns, options = {})
    from_columns.each do |from_column|
      foreign_key(from_table, from_column, options)
    end
  end

In the first invocation of foreign_key options[:to_table] will be set (if it isn't set already) to the destination table of the first column. The options[:to_table] value will be retained throughout the loop causing all foreign keys to point to the same table. The fix is to make to_table a local variable or to do add an "options = options.dup" line at the beginning of the method.

Lesson learned - avoid modifying the options hash and any other arguments that the caller doesn't expect will be modified.

Comments

Paul Doerwald said over 6 years ago:

Well said... and a humbling point as I realize I've committed this sin a few times (I should have known better!). I have some code to correct...!

--------------------------------------------------------------------------------