Hi,

On Sat, Jun 6, 2009 at 5:19 PM, Yehuda Katz <wycats / gmail.com> wrote:
> Matz,
> I've been working on adding some specs to RubySpec related to the ducktyping
> interface. I have observed the following behavior and want to know whether
> or not it's intentional. Let's use the #to_s coercion as an example:
> 1. If the object has a method #to_s, it's called.
> 2. If #to_s returns something that is not a String, a TypeError is raised.
> In MRI, the mechanism for determining if an object has a method #to_s is to
> call respond_to?. This means that even if the method does not exist, #to_s
> will get called, and fall through to #method_missing, if the #respond_to
> method returns true for :to_s. As a result, when using MRI, it is possible
> to implement the duck-typing interface without defining #to_s, but instead
> defining a combination of #respond_to? and #method_missing that returns a
> String.

Here are some example specs that illustrate the two ideas. Currently,
the RubySpecs are silent about whether #respond_to? *must* be called.
The basic specs are like these:

describe "Kernel#String()" do
  it "calls #to_s to convert an arbitrary object to a String" do
    obj = mock('test')
    obj.should_receive(:to_s).and_return("test")

    String(obj).should == "test"
  end

  it "raises a TypeError if #to_s does not exist" do
    obj = mock('to_s')
    obj.undefine(:to_s)

    lambda { String(obj) }.should raise_error(TypeError)
  end

  it "raises a TypeError if #to_s does not return a String" do
    (obj = mock('123')).should_receive(:to_s).and_return(123)
    lambda { String(obj) }.should raise_error(TypeError)
  end

  it "returns the same object if it is already a String" do
    string = "Hello"
    string.should_not_receive(:to_s)
    string2 = String(string)
    string.should equal(string2)
  end

  it "returns the same object if it is an instance of a String subclass" do
    subklass = Class.new(String)
    string = subklass.new("Hello")
    string.should_not_receive(:to_s)
    string2 = String(string)
    string.should equal(string2)
  end
end

If #respond_to? must be called, these specs are added to the ones above:

describe "Kernel#String()" do
  # ...

  it "raises a TypeError if respond_to? returns false for #to_s" do
    obj = mock("to_s")
    obj.does_not_respond_to(:to_s)

    lambda { String(obj) }.should raise_error(TypeError)
  end

  it "raises a NoMethodError if #to_s is not defined but
#respond_to?(:to_s) returns true" do
    obj = Object.new
    obj.undefine(:to_s)
    obj.responds_to(:to_s)

    lambda { String(obj) }.should raise_error(NoMethodError)
  end

  it "calls #to_s if #respond_to?(:to_s) returns true" do
    obj = mock('to_s')
    obj.undefine(:to_s)
    obj.fake!(:to_s, "test")

    String(obj).should == "test"
  end
end

Also, whatever is the decision about this, can we please clarify
whether it applies only to String(), Float(), Integer(), or whether it
applies to any place that #to_s, #to_i, #to_int, etc. are called in
MRI. Another way to phrase this is: If MRI calls
rb_funcall(some_method) inside of method #foo, should calling
#some_method be considered part of the public interface of method
#foo.

Thanks,
Brian

> Is this intentional? Would it be ok for an alternative implementation to
> look up the #to_s method using an internal check instead of calling the
> user-defined #respond_to?, as MRI does?
> In other words, is the only "correct" way to enlist in Ruby coercion to
> define to_s explicitly, or is another correct way to define respond_to? and
> method_missing to return an appropriate response?
> --
> Yehuda Katz
> Developer | Engine Yard
> (ph) 718.877.1325
>