I have a class which exposes a string value and an int value (a command output and exit code respectively). In addition to exposing them through to_s
and to_i
, I'm also using to_str
and to_int
, like so:
class Status
def to_s
@output
end
alias :to_str :to_s
def to_i
@status.exitstatus
end
alias :to_int :to_i
end
My idea behind this is to be able to use this object in as many situations as possible. having it coerceable to a string or int increases that usability. For instance, I can concatenate the object with a string:
a_string = "Output was: " + results
(I wanted to use this as an example of int coercion, but Fixnum.+ doesn't like it, so it doesn't actually work:)
an_int = 1 + results
Everything I've read so far has said that this is probably a "bad" thing to do. The common theme goes like this: "Use to_s
/to_i
when your object can be represented as a string/int, but to_str
/to_int
only if your object is fundamentally a string/int".
There's no question that my class is not "fundamentally" a string or an int. However I have some problems with this rule:
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).So, my question is: is there any real (ie: practical) harm in implementing to_str
and to_int
?
Update: Jörg W Mittag gave an example that made me think of something. To rephrase the question: is there really a need to have to_str
/to_int
when you already have to_s
/to_i
? (Besides the fact that particular methods are already expecting to_str
over to_s
)
For instance, in Jörg's Array.join example, the array members are converted via to_s while the separator is converted via to_str. But is this really necessary? If Array.join called separator.to_s instead, then you could successfully pass many more objects to it (ex: Integers, Symbols, etc) and gain that much more flexibility. Does Ruby benefit from having this separation?
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 haveStatus#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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With