Most object oriented programming languages provide an interface that an object can implement to describe itself as a string, so when it is given as an argument to a “printing” function, it gets converted/coerced to it’s desired format.

In Ruby, this is achieved by implementing the to_s method, so if you define a class implementing this method, puts will behave like this:

>> class MyClass
>>   def to_s
>>     "<MyClass Instance>"
>>   end
>> end
=> nil
>> instance = MyClass.new
=> <MyClass Instance>
>> puts instance
<MyClass Instance>
=> nil

So everything is well and works as expected until you suddenly decide to subclass the Array or the String class:

>> class MyString < String
>>   def to_s
>>     "<MyString #{super}>"
>>   end
>> end
=> nil
>>
>> class MyArray < Array
>>   def to_s
>>     "<MyArray instance>"
>>   end
>> end
=> nil
>>
>> mystring = MyString.new("Hello World!")
=> "Hello World!"
>>
>> myarray = MyArray.new
=> []
>>
>> puts mystring
Hello World!
=> nil
>> puts myarray
=> nil

Wait, what!? As you can see, I’ve implemented the to_s function but it didn’t get called when I passed the instances to the puts function. That means I did something wrong, right?

>> mystring.to_s
=> "<MyString Hello World!>"
>> myarray.to_s
=> "<MyArray instance>"

Well, apparently not. So what kind of trickery is going on here? I’ve implemented the necessary method but it’s not being called for those specific subclasses, so it’s bound to be an implementation detail of the function we are calling. Let’s take a look at how puts is implemented in Ruby 1.

Opening up the io.c file and searching for “puts” we end up finding a declaration of the rb_io_puts function:

VALUE
rb_io_puts(int argc, VALUE *argv, VALUE out)
{
    int i;
    VALUE line;

    /* if no argument given, print newline. */
    if (argc == 0) {
        rb_io_write(out, rb_default_rs);
        return Qnil;
    }
    for (i=0; i<argc; i++) {
        if (TYPE(argv[i]) == T_STRING) {
            line = argv[i];
            goto string;
        }
        line = rb_check_array_type(argv[i]);
        if (!NIL_P(line)) {
            rb_exec_recursive(io_puts_ary, line, out);
            continue;
        }
        line = rb_obj_as_string(argv[i]);
    string:
        rb_io_write(out, line);
        if (RSTRING_LEN(line) == 0 ||
            !str_end_with_asciichar(line, '\n')) {
            rb_io_write(out, rb_default_rs);
        }
    }

    return Qnil;
}

The comment block above the function definition already explains what happens, but let’s understand the function and why it behaves the way it does when we call it with our custom Array and String subclasses.

TYPE is a macro that expands to rb_type, which ultimately checks the .flags field from the object cast as struct RBasic to identify it’s type. So, for the string behavior it’s simple:

TYPE(argv[i]) == T_STRING is True, which makes the code jump to the string label and simply write it’s contents.

For the array case, the code calls rb_check_array_type (from array.c), which cascades to testing TYPE(argv[i]) == T_ARRAY && T_ARRAY != T_DATA. That results in true, so the code enters the if (!NIL_P(line)) block, calling io_puts_ary (that ends up calling rb_io_puts for each item of the array) and goes to handle the next parameter.

Now, only if both tests above fails the interpreter ends up calling rb_obj_as_string, which, as you may have guessed, is responsible for calling the higher level to_s implementation.

So now you might be thinking “if I don’t subclass Array nor String ruby will certainly call my to_s implementation, right?”. Well, wrong! :)

If you’ve been following along what I’ve written with Ruby’s source code, you might have noticed the following:

VALUE
rb_check_array_type(VALUE ary)
{
    return rb_check_convert_type(ary, T_ARRAY, "Array", "to_ary");
}

Notice the last parameter being given (to_ary)? It’s a well known friend of rubyists: it’s a method that converts an object to array.

Without digging deeper in the code, what might this mean? Let’s do a little test. Remember our first example, the MyClass implementation? We’ll build up on that:

>> class MyClass
>>   def to_s
>>     "<MyClass Instance>"
>>   end
>> end
=> nil
>> instance = MyClass.new
=> <MyClass Instance>
>> puts instance
<MyClass Instance>
=> nil
>> class MyClass
>>   def to_ary
>>     %W(MyClass Instance as Array)
>>   end
>> end
=> nil
>> puts instance
MyClass
Instance
as
Array
=> nil

Weird isn’t it? Turns out that rb_check_array_type returns a valid pointer if the parameter implements the to_ary method. This means that whenever an object has this method implemented, it will be treated as an array by puts and the to_s method will never be called by it.

And that’s it, phew! To summarise: puts does call to_s as long as you don’t:

  1. Subclass from String nor Array
  2. Implement the to_ary method

Note: This post idea comes from a question I’ve answered on a Portuguese stackoverflow-like website some months ago.


  1. I’m using the C reference implementation for Ruby, version 1.9.2-p290, obtainable at http://ruby-lang.org/.