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
Activerecord Objects in Hashes Aren't Garbage Collected -- a Bug or a Sort of Caching Feature
Ruby: Select a Hash from Inside an Array
Library Not Loaded: /Usr/Local/Opt/Readline/Lib/Libreadline.6.Dylib (Loaderror)
Rails - How to Check Developer Mode or Production Mode in Code
How to Split a String in Ruby and Get All Items Except the First One
Most Concise Way to Test String Equality (Not Object Equality) for Ruby Strings or Symbols
How to Use Ruby Minitest::Spec with Rails for API Integration Tests
Using a Ruby Script to Login to a Website via Https
Instance and Class Variables in Rails Controller
Rails Flash Message Remains for Two Page Loads
Rails: I Can't Call a Function in a Module in /Lib - What am I Doing Wrong
How to Write Specs for Code That Depends on Environment Variables
Multiple Robots.Txt for Subdomains in Rails