Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is #{} always equivalent to to_s?

In "What does it mean to use the name of a class for string interpolation?", Candide suggested that #{} inside a string implicitly calls to_s. So, for instance:

my_array = [1, 2, 3, 4]
p my_array.to_s # => "[1, 2, 3, 4]"
p "#{my_array}" # => "[1, 2, 3, 4]"

However, if to_s for Array is redefined as shown below, I would get different results:

class Array
  def to_s
    self.map { |elem| elem.to_s }
  end
end

p my_array.to_s # => ["1", "2", "3", "4"]
p "#{my_array}" # => "#<Array:0x007f74924c2bc0>"

I suppose this happens any time and anyhow to_s is overridden.

What am I supposed to do to keep the equality between to_s and the expression #{} in a string, if possible?

I came across this issue in a RubyMonk lesson: what according to the lesson #{ogres} should return, according to my experience is something different.

like image 612
Asarluhi Avatar asked Mar 14 '23 18:03

Asarluhi


1 Answers

Please take a look at the documentation for Object#to_s. It says that to_s should return a String. When you override a method, you should always honor its contract. Take a look at the documentatio for Array#to_s As you can see, it also returns a String. [Note that this is true for all the to_X and the to_XYZ methods: they must always return an object of the corresponding class and they must not raise an Exception or otherwise fail.]

Your implementation of to_s, however, does not return a String. It returns an Array, thus violating to_s's contract. Once you violate a method's contract, all bets are off. Personally, I think it would be more appropriate to raise a TypeError exception here, but Ruby is trying to be nice and returns some String instead, which (in this case) prints the class name and some unique identifier.

Here is the commit to the RubySpec project which (implicitly) states that no Exception is raised and explicitly states that an implementation-defined but otherwise unspecified String is returned: The spec for interpolation when Object#to_s did not return a String was confusing the default representation of an arbitrary object and Object#inspect.

The latest version of the spec, before the project was closed looks like this language/string_spec.rb#L197-L208:

it "uses an internal representation when #to_s doesn't return a String" do
  obj = mock('to_s')
  obj.stub!(:to_s).and_return(42)

  # See rubyspec commit 787c132d by yugui. There is value in
  # ensuring that this behavior works. So rather than removing
  # this spec completely, the only thing that can be asserted
  # is that if you interpolate an object that fails to return
  # a String, you will still get a String and not raise an
  # exception.
  "#{obj}".should be_an_instance_of(String)
end

As you can see, all that is guaranteed in this case, is that you won't get an Exception, and that you will get a String, however, it says nothing about what that String looks like.

like image 179
Jörg W Mittag Avatar answered Mar 27 '23 06:03

Jörg W Mittag