It makes my class less flexible/usable. For example: I couldn't use String#+
to concatenate the Status output with the other string if I didn't have Status#to_str
.
This is a bad example, because concatenating strings is unidiomatic Ruby. String interpolation is the preferred way:
a_string = "Output was: #{results}"
And this just works™, because string interpolation actually calls to_s
on the result of the interpolated expression.
It seems to violate the spirit of duck-typing. The user of an object (ie: a method that gets it as a parameter) shouldn't care what that object is, it should only care what it can do. (In this case, "do" means "can be represented as a string/int".)
I would argue that "can be represented as a string/int" isn't really behavior. IOW: "what the object can do" is about interesting behavior in a certain context, and "can be represented as a string/int" isn't really interesting behavior.
If you say that "Status IS-A Integer" (which is essentially what to_int
means), then it follows that you can, for example, do arithmetic with it. But what does it even mean to "add 42 to file not found"? What is the logarithm of success? What is the square root of failure?
In the above string interpolation example, the interesting behavior is "can be displayed". And that is basically indicated by implementing #to_s
. Concatenating two strings, OTOH, requires, well, two strings.
The arguments for "is fundamentally a string/int" are pretty fuzzy to me. For example, you'll see that Float#to_int
is mentioned a lot. The story goes that since a floating-point number always has an integer component, to_int
is a valid method. However, I think this is spurious: a Float is not an integer (as it has a non-integer component) and so trying to equate their "typeness" doesn't make much sense. You can legitimately convert a Float to an integer (through truncation), but then I can say that I can convert my Status to an integer as well (by "truncating" all of the non-exit-code information).
Again, this is a rather weak argument, because I actually agree with you: that's wrong.
In German Law, we have a principle which is hard to grasp and un-inituitive, but which I think applies here perfectly. It is called "Keine Gleichheit im Unrecht" (No Equality in Wrongness). It means that the basic right of Equaliy, which is granted in the Constitution, only applies within the Law. To put it another way: OJ doesn't make murder legal.
So, just because there is crap code in the Ruby core library (and believe me, there is a lot), doesn't mean you get to write crap, too :-)
In this particular case, Float#to_int
is just plain wrong and shouldn't exist. Float
is not a subtype of Integer
. At first glance, the opposite seems to be true, i.e. Integer#to_float
is valid, but actually that's not true, either: in Ruby, Integer
s have arbitrary precision, but Float
s have fixed precision. It would be valid to implement Fixnum#to_float
, but that would be a bad idea, since Integer
s can magically convert from Fixnum
to BigInteger
and back, and thus the #to_float
method would "magically" appear and vanish.
The thing that finally helped me understand the difference between to_x
and to_xyz
was Array#join
: it prints out the elements of the array, separated by a separator object. It does this by calling to_s
on each element of the array and to_str
on the separator. Once you understand why it calls to_s
on one and to_str
on the other, you're basically set.
(Although your comments on Float#to_int
already indicate that you do understand.)
Side note: for double dispatch in an algebraic context, Ruby actually uses the #coerce
protocol. So, if you wanted the 1 + a_status
example to work, you would need to implement Status#coerce
. But, please don't.