Hash with Indifferent Access

If I have a hash in Ruby on Rails, is there a way to make it indifferent access?

You can just use with_indifferent_access.

SETTINGS = YAML.load_file("#{RAILS_ROOT}/config/settings.yml").with_indifferent_access

Select on hash with indifferent access not working

All you really get with HashWithIndifferentAccess is the ability to set and get values using either a string or a key. Once you start using other reading methods on the hash you move to objects that are not indifferent to strings or symbols.

However, HashWithIndifferentAccess does help you because:

Internally symbols are mapped to strings when used as keys in the entire writing interface (calling []=, merge, etc)

....

You are guaranteed that the key is returned as a string

This means that you're always going to get a string for keys with methods like select:

> h = { sym_key: 'sym_value', 'string_key' => 'string_value' }.with_indifferent_access
> h.keys
=> ["sym_key", "string_key"]

Hash with indifferent access

Let the config hash return the value for the stringified version of the key:

config = {"host"=>"value1", "Username"=>"Tom"}
config.default_proc = proc{|h, k| h.key?(k.to_s) ? h[k.to_s] : nil}
p config[:host] #=> "value1"

The default_proc runs everytime when a key is not found in the hash. Note this is only half of indifferent access: config["host"] will result in nil if the key :host is present. If that has to work too:

config.default_proc = proc do |h, k|
case k
when String then sym = k.to_sym; h[sym] if h.key?(sym)
when Symbol then str = k.to_s; h[str] if h.key?(str)
end
end

See the comments about limitations of this approach (tltr: separate values for :a and 'a' are possible, does not take into account Hash.delete and others).

Strings vs Symbols when using HashWithIndifferentAccess?

Ruby Strings are mutable, which can bring some unpredictability and reduced performance. For these reasons Ruby also offers the choice of Symbols. The big difference would be that Symbols are immutable. While mutable object can be changed the immutable ones can only be overwritten. Symbols play more nicely with the memory and thus gain on performance, but if not careful enough the memory footprint of your app will increase.

Using Strings or Symbols comes down to understanding both of the terms and how they will serve the purpose in benefit of the overall app health and performance. This is the reason why maybe there is no strict definition of where to use String and where Symbol.

What I would maybe give as a guidance of what is recommended where:

  • Symbol - internal identifiers, variables not meant to change
  • String - variables that are changing or printed out

HashWithIndifferentAccess ends up mapping internally all the symbols to strings.

h = ActiveSupport::HashWithIndifferentAccess.new(test: 'test')

If you try to retrieve the keys of the created hash (h) you will get the keys as strings

h.keys # => ["test"]

Difference between Ruby’s Hash and ActiveSupport’s HashWithIndifferentAccess

Below is the simple example that will show you difference between simple ruby hash & a "ActiveSupport::HashWithIndifferentAccess"

  • HashWithIndifferentAccess allows us to access hash key as a symbol or string

Simple Ruby Hash

$ irb
2.2.1 :001 > hash = {a: 1, b:2}
=> {:a=>1, :b=>2}
2.2.1 :002 > hash[:a]
=> 1
2.2.1 :003 > hash["a"]
=> nil

ActiveSupport::HashWithIndifferentAccess

2.2.1 :006 >   hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1, b:2)
NameError: uninitialized constant ActiveSupport
from (irb):6
from /home/synerzip/.rvm/rubies/ruby-2.2.1/bin/irb:11:in `<main>'
2.2.1 :007 > require 'active_support/core_ext/hash/indifferent_access'
=> true
2.2.1 :008 > hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1, b:2)
=> {"a"=>1, "b"=>2}
2.2.1 :009 > hash[:a]
=> 1
2.2.1 :010 > hash["a"]
=> 1
  • class HashWithIndifferentAccess is inherited from ruby "Hash" & above special behavior is added in it.

Why does Rails' `HashWithIndifferentAccess` store keys as strings and not symbols?

Why have they chosen to do this

ActiveSupport::HashWithIndifferentAccess is mainly used to handle parameters coming from outside. Symbols are stored in the ruby heap and in general, they are never released back to the system.

Using symbols as keys in something that gets keys from the outside, leads to the vulnerability against OutOfMemory attacks ([D]DoS, sending queries with randomly generated parameters names.) That’s why strings were chosen, AFAIU. For 100% assurance ask DHH.

how significant is the difference in performance?

Use Benchmark to check. This site is not supposed to be a “please do benchmarks for me” site.

ruby/hash:ActiveSupport::HashWithIndifferentAccess

TL;DR:

serialize :title, Hash

What’s happening here is that serialize internally will yaml-dump the class instance. And the hashes in rails are monkeypatched to may having an indifferent access. The latter means, that you are free to use both strings and respective symbols as it’s keys:

h = { 'a' => 42 }.with_indifferent_access
puts h[:a]
#⇒ 42

Rails return JSON serialized attribute with_indifferent_access

use the built-in serialize method :

class Whatever < ActiveRecord::Base
serialize :params, HashWithIndifferentAccess
end

see ActiveRecord::Base docs on serialization for more info.

Freezing HashWithIndifferentAccess in Ruby

So we're looking at how the nested hash behaves differently between

v_1 = { a: { b: 2 } }.with_indifferent_access
v_2 = { a: { b: 2 }.with_indifferent_access }.with_indifferent_access

When you call Hash#with_indifferent_access, it creates a new ActiveSupport::HashWithIndifferentAccess object; and then calls #update to insert all of the key/value pairs from the original hash into the new object (ActiveSupport::HashWithIndifferentAccess#update), which calls #convert_values with the nested hash:

def convert_value(value, options = {})
if value.is_a? Hash
if options[:for] == :to_hash
value.to_hash
else
value.nested_under_indifferent_access
end
...

So both { b: 2 } and { b: 2 }.with_indifferent_access will have #nested_under_indifferent_access called on them. But that’s a different method for Hash than it is for HashWithIndifferentAccess. In the core_ext file, Hash#nested_under_indifferent_access calls HashWithIndifferentAccess.new(self), but HashWithIndifferentAccess#nested_under_indifferent_access just returns self.

So {'b' => 2}.nested_under_indifferent_access returns a new object, but {'b' => 2}.with_indifferent_access.nested_under_indifferent_access doesn’t do anything to the object. Which is why if the first one is frozen, you get back a different (unfrozen by default) object, and if the second one is frozen, it stays frozen.

Is it bad to completely replace Hash with HashWithIndifferentAccess in Ruby?

What do you think {}.class is going to be after your kludge? Right, it will be Hash.

What do you think Hash[:a, 'a'] will do? Right, SystemStackError for hopefully obvious reasons.

So yes, this is a bad idea, you will regret it, and an important downside is that it doesn't actually do what you think it will do.



Related Topics



Leave a reply



Submit