Implicit vs explicit type conversions in Ruby (to_h/to_hash and others)
Just a quick reference post.
There are several pairs of type coercions methods in Ruby:
to_iandto_int;to_sandto_str;to_aandto_ary;to_handto_hash.
What’s the difference and when to use/implement each of pair?
Shorter version is explicit conversion:
- your type should implement if it can be somehow converted into target type (integer, string, array and so on);
- you should call those method on incoming data, if you expect it to have some means of converting to target type.
Longer version is implicit conversion:
- your type should implement it only if it sees itself as “kind of” target type (“better” or “specialized” number, string, array, hash);
- you should call those methods on incoming data, if you have strict requirement for it being of target type, but want to check it in Ruby way, with duck typing, not class comparison;
- you should not implement and use this methods in other cases!
Most of Ruby operators and core methods use implicit conversions on data:
"string" + otherwill call#to_stronother,[1,2,3] + otherwill call#to_aryand so on (and will raiseTypeError: no implicit conversionif object not responds to this methods);"string" * otherand[1,2,3] * otherwill call#to_intonother;a, b, c = *otherwill callother#to_ary(and, therefore, can be used to “unpack” custom collection objects—but also can cause unintended behavior, ifotherimplementsto_arybut was not thought as a collection);some_method(*other)—same as above, usesother#to_ary;some_method(**other)—new in Ruby 2, usesother#to_hash.
I should repeat: never implement implicit conversion methods unless
you sure know what you are doing! It is widely seen, for example, the
#to_hash method being implemented (maybe because of “prettier name”
than #to_h?) and causing strangest effects.
Practical example:
class Dog
attr_reader :name, :age
def initialize(name, age)
@name, @age = name, age
end
# erroneously defined for serialization instead of to_h
def to_hash
{species: 'Dog', name: name, age: age}
end
end
def set_options(**some_options)
p some_options
end
dog = Dog.new('Rex', 5)
set_options(foo: 'bar')
# ok, prints {foo: 'bar'}
set_options(1)
# can't be called this way, raises helpful ArgumentError
set_options(dog)
# ooops, treats our dog as an options hash
# and prints {species: 'Dog', name: name, age: age}
require 'awesome_print' # pretty popular pretty printing gem
ap dog
# ooops, uses Hash pretty-printing method
# instead of Dog#inspectSee also: