Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Consequences of implementing to_int and to_str in Ruby

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:

  1. 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.
  2. 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".)
  3. 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).

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?

like image 641
Craig Walker Avatar asked Nov 11 '09 21:11

Craig Walker


1 Answers

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, Integers have arbitrary precision, but Floats have fixed precision. It would be valid to implement Fixnum#to_float, but that would be a bad idea, since Integers 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.

like image 157
Jörg W Mittag Avatar answered Sep 30 '22 09:09

Jörg W Mittag