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 >