All about to_h in Ruby 2.0
I gave a talk at ICRuby today about Ruby 2.0, partially as a learning experience for myself. I hadn’t done much with Ruby 2.0 before, and I had fun learning more about what to expect. If you’d like to see what I presented, my slides are available.
A lot of what I showed about Ruby 2.0 was a pretty standard overview, but I paid special attention to to_h
. I ended up doing some research that I haven’t seen written up elsewhere, and thought I should share it as a blog post as well.
What’s to_h
?
It’s a new convention for retrieving a Hash
representation of an object. When I was first learning Ruby in 2008, I remember expecting to_h
to exist after learning about to_s
for String
and to_a
for Array
. In concept, it’s similar to serializable_hash
in Rails as well. I’m happy to see this become a part of core Ruby.
What can I do with it?
Now that there’s an official method for getting the “Hash
version” of an object, you can start to use it in your methods when using a Hash
makes things easier, or you’d like to duck type.
Since to_h
is now a part of Ruby’s core and std-lib, I thought I’d see how it’s used.
A quick word about the examples: we tend to use panda
and bamboo
as metasyntactic variables at Continuity Control, partially because of the great Jonathan Magen. Plus, they’re fun compared to plain old foo
and bar
.
Core
Searching ruby-doc.org core gave:
…and a handful of aliases called to_hash
, which were also present in 1.9.
Here’s what they do:
ENV.to_h # => {"TERM"=>"xterm", "SHELL"=>"/bin/bash", ...}
{ panda: 'bamboo' }.to_h # => {:panda=>"bamboo"}
nil.to_h # => {}
s = Struct.new(:panda, :bamboo).new
s.to_h # => {:panda=>nil, :bamboo=>nil}
Std-lib
Searching ruby-doc.org std-lib gave:
- Useful:
- Less common:
JSON::Ext::Generator::State
XMLRPC::FaultException
OpenSSL::X509::Extension
I don’t have any examples for the latter 3, but OpenStruct#to_h
is easy to demonstrate:
require 'ostruct'
os = OpenStruct.new(panda: 'bamboo')
os # => #<OpenStruct panda="bamboo">
os.to_h # => {:panda=>"bamboo"}
Any gotchas?
Not everything about to_h
works the way I’d expect.
Arrays
This doesn’t work:
%w(panda bamboo).to_h # => NoMethodError: undefined method `to_h'
I might have expected behavior like this:
Hash['panda', 'bamboo'] # => {"panda"=>"bamboo"}
That would be especially nice, since then you could convert back and forth from Array
to Hash
:
{ panda: 'bamboo' }.to_a.to_h # => NoMethodError: undefined method `to_h'
…but alas, that’s just not how it works. However, we can try to convince matz otherwise.
Something I found by accident: I screwed up Hash[]
the first time and got a bunch of new warnings on STDERR.
Hash[['panda', 'bamboo']]
# (irb):5: warning: wrong element type String at 0 (expected array)
# (irb):5: warning: ignoring wrong elements is deprecated, remove them explicitly
# (irb):5: warning: this causes ArgumentError in the next release
In Ruby 1.9.3, it would print no warnings and simply return {}
.
JSON
I also was hoping JSON would take advantage of to_h
, since it’s now a part of Ruby’s stdlib.
require 'json'
JSON.generate(ENV)
# /usr/local/lib/ruby/2.0.0/json/common.rb:223:in `generate': only generation of JSON objects or arrays allowed (JSON::GeneratorError)
# from /usr/local/lib/ruby/2.0.0/json/common.rb:223:in `generate'
# from tmp/talk_code.rb:3:in `<main>'
I would have expected something like this:
require 'json'
# NOTE: This doesn't actually work this way. Blog skimmers take notice!
JSON.generate(ENV) # => "{\"TERM\":\"xterm\",\"SHELL\": ...
Fortunately, you can do this:
require 'json'
JSON.generate(ENV.to_h) # => "{\"TERM\":\"xterm\",\"SHELL\": ...
…but that feels like an excellent use of to_h
that should have been a part of JSON
.
Enough complaining! Show something useful.
Duck typing is probably the most useful use case I can think of. It’s a good alternative to Hash[]
in some cases as well.
to_h
Here’s a simple example. Let’s say I have a method called eat
:
def eat(diet)
"A panda eats #{ diet[:eats] }"
end
…but I want to make sure the diet
that is passed in is treated like a Hash
. That’s possible now:
def eat(diet)
"A panda eats #{ diet.to_h[:eats] }"
end
# It works with a Hash
panda_diet = { eats: 'bamboo' }
eat(panda_diet) # => "A panda eats bamboo"
# ...a Struct
panda_diet = Struct.new(:eats).new('shoots and leaves')
eat(panda_diet) # => "A panda eats shoots and leaves"
# ...or even nil
eat(nil) # => "A panda eats "
Hash()
One other addition I noticed (but haven’t seen mentioned elsewhere) is the new Hash()
method. It’s kind of like Array()
or String()
in Ruby 1.9.3 and below. These methods let you coerce values, in a sense.
Here’s an example using Array
:
def eat_up(foods)
# Turns anything into an `Array`:
#
# nil => []
# 'bamboo' => ['bamboo']
# ['bamboo'] => ['bamboo']
#
Array(foods).each do |food|
puts eat(eats: food)
end
end
eat_up(nil)
# [no output]
eat_up('bamboo')
# A panda eats bamboo
eat_up(['bamboo', 'shoots and leaves'])
# A panda eats bamboo
# A panda eats shoots and leaves
That’s actually very useful behavior; a lot of annoying type and error checking just goes away. Ever since I first saw Avdi Grimm present it, I’ve found many uses for it.
The good news is, you can now do something similar with Hash()
def eat(diet)
diet = Hash(diet)
"A panda eats #{ diet[:eats] }"
end
panda_diet = { eats: 'bamboo' }
eat(panda_diet) # => "A panda eats bamboo"
eat(nil) # => "A panda eats "
If used in the right situation, that might just be as useful as Array()
.
But strangely enough, Hash()
doesn’t work exactly like to_h
:
Hash([]) # => {}
Hash(OpenStruct.new)
# TypeError: can't convert OpenStruct into Hash
# from (irb):100:in `Hash'
# from (irb):100
# from /usr/local/bin/irb:12:in `<main>'
I don’t currently have an explanation, but unless you need specific behavior from Hash()
, you may prefer to use to_h
.
Slightly less contrived examples
Flexible constructor
If you have a use case for it, to_h
could make constructors more flexible:
class Panda
def initialize(params)
params = params.to_h
@name = params[:name]
@age = params[:age]
@weight = params[:weight]
end
end
Flexible constructor with OpenStruct
Or even use an OpenStruct
instead, if you’d like:
require 'ostruct'
class Panda
def initialize(params)
params = OpenStruct.new(params.to_h)
@name = params.name
@age = params.age
@weight = params.weight
end
end
OpenStruct
conversion
If you felt like it, you could even refactor that into this:
require 'ostruct'
# Convert to an `OpenStruct`
def OpenStruct(hash_like)
OpenStruct.new(hash_like.to_h)
end
env = OpenStruct(ENV)
env.TERM # => 'xterm'
Reusable to_h
definition
You could even parameterize how to define to_h
:
# Related to my concept of `to_h` back in 2010: https://github.com/benjaminoakes/snippets/blob/master/ruby/to_h.rb
module ConversionHelper
def pick(*methods)
h = {}
methods.each { |m| h[m] = send(m) }
h
end
end
class Panda
include ConversionHelper
attr_reader :name, :age
def initialize(params)
params = OpenStruct.new(params.to_h)
@name = params.name
@age = params.age
@weight = params.weight
end
def to_h
# Pandas are sensitive about their weight, after all...
pick(:name, :age)
end
end
I haven’t decided whether the last few ideas would actually be useful in practice, but these are the types of things that Hash()
and to_h
open up for Rubyists.