Ruby type coercions

When developing in Ruby, we may have to convert our own custom objects into one of the built-in system types.

Ruby has built-in support for conversion protocols. By that, it means Ruby allows an object of a class to have itself converted to an object of another class.

Ruby supports three main modes of type conversions: lenient, strict, numeric. We will discuss the first two forms of conversions in this post.

When you call to_s or to_i on a receiver, Ruby looks for an equivalent representation in the receiver. For instance, calling to_s on any class which does not implement a custom to_s method will return just the class name followed by the object ID as a string.

Sometimes, strict conversions are necessary in order to use our custom objects interchangeably with the built-in system types - we need our custom objects to work together with the core system types.

This is when the strict conversion functions from the standard library comes in handy.

Below is a list of these predefined system methods and their corresponding conversions:

to_a      ->  Array

to_ary    ->  Array

to_enum   ->  Enumerator

to_hash   ->  Hash

to_int    ->  Integer

to_io     ->  IO

to_open   ->  IO

to_path   ->  String

to_proc   ->  Proc

to_regexp ->  Regexp

to_str    ->  String

to_sym    ->  Symbol

Some of the Ruby built-in classes such as Array, Hash, Regexp, String support the try_convert method call, which tries to transform the object passed as its argument into the corresponding type.

When try_convert is invoked on an object, the type it is to be converted to looks for the associated method call in the list above in order to determine how the conversion is to occur.

Below is an example of transforming a custom class of type Alphabet into an array by implementing to_ary:

1 class Alphabet
2   def to_a
3     ('a'..'z').to_a
4   end
5 end
6 
7 res = Array.try_convert(Alphabet.new) # => calls to_ary on class A
8 puts res.class # => Array
9 puts res # => ['a', 'b', 'c',...,'z']

This allows the use of the Alphabet class interchangeably when an array is expected.

However, we are not limited to just a single implementation.

Taking the example above, if we need to extend the conversions to the other classes, we can also add the other corresponding methods:

 1 class Alphabet
 2   def alpha_range
 3     ('a'..'z')
 4   end
 5 
 6   # returns array of alphabets
 7   def to_ary
 8     alpha_range.to_a
 9   end
10 
11   # returns hash of alphabet as key and its position as value
12   def to_hash
13     ('a'..'z').each_with_object({}){ |alpha, hsh|
14       hsh[alpha] = alpha_range.find_index(alpha)
15     }
16   end
17 
18   # returns string of all alphabets concatenated
19   def to_str
20     self.to_ary.join(' ')
21   end
22 
23   # calls to_str to return a Regexp
24   def to_regexp
25     Regexp.new(self)
26   end
27 
28   # returns number of alphabets in range
29   def to_int
30     alpha_range.count
31   end
32 end
33 
34 alphabets = Alphabet.new
35 
36 Array.try_convert(alphabets) # => ['a', 'b', 'c',...,'z']
37 
38 Hash.try_convert(alphabets) # => {"a"=>0, "b"=>1, "c"=>2, ..., "z" => 25}
39 
40 String.try_convert(alphabets) # => 'a b c ... z'
41 
42 Regexp.try_convert(alphabets) # => /a b c .... z/
43 
44 Integer(alphabets)            # => 26

Keep hacking and stay curious!