在 Ruby 中实现 to_int 和 to_str 的后果

发布于 2024-08-10 10:42:33 字数 1798 浏览 7 评论 0原文

有一个类,它公开了一个字符串值并且一个 int 值(分别是命令输出和退出代码)。除了通过 to_sto_i 公开它们之外,我还使用 to_strto_int,如下所示:

class Status
  def to_s
    @output
  end
  alias :to_str :to_s

  def to_i
    @status.exitstatus
  end
  alias :to_int :to_i
end

我的想法是能够在尽可能多的情况下使用这个对象。将其强制转换为字符串或整数可以提高可用性。例如,我可以将对象与字符串连接起来:(

a_string = "Output was: " + results

我想用它作为 int 强制转换的示例,但 Fixnum.+ 不喜欢它,所以它实际上不起作用:)

an_int = 1 + results

我读过的所有内容到目前为止,这可能是一件“坏”事。常见的主题是这样的:“当你的对象可以表示为 string/int 时,使用 to_s/to_i,但是 to_str/to_int 仅当您的对象基本上是一个字符串/int”。

毫无疑问,我的类“从根本上”不是字符串或整数。然而 我对这条规则有一些问题:

  1. 它使我的班级不太灵活/可用。例如:如果没有 Status.to_str,我无法使用 String.+ 将 Status 输出与其他字符串连接起来。
  2. 这似乎违背了鸭子打字的精神。对象的用户(即:将其作为参数获取的方法)不应该关心该对象是什么,而应该只关心它可以做什么。 (在这种情况下,“do”意味着“可以表示为字符串/整数”。)
  3. “本质上是字符串/整数”的参数对我来说非常模糊。例如,您会看到 Float.to_int 被多次提及。故事是这样的,由于浮点数总是有一个整数部分,所以 to_int 是一个有效的方法。然而,我认为这是虚假的:Float不是一个整数(因为它有一个非整数分量),因此尝试等同它们的“类型”没有多大意义。您可以合法地将 Float 转换为整数(通过截断),但是我可以说我也可以将我的 Status 转换为整数(通过“截断”所有非退出代码信息)。

所以,我的问题是:实施 to_strto_int 是否有任何真正的(即实际的)危害?


更新:Jörg W Mittag 举了一个例子,让我想到了一些事情。重新表述一下问题:当您已经拥有 to_s/to_i 时,是否真的需要拥有 to_str/to_int ? (除了特定方法已经期望 to_str 而不是 to_s

例如,在 Jörg 的 Array.join 示例中,数组成员在转换分隔符时通过 to_s 进行转换通过 to_str。但这真的有必要吗?如果 Array.join 调用separator.to_s,那么您可以成功地将更多对象传递给它(例如:整数、符号等)并获得更大的灵活性。 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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

鸵鸟症 2024-08-17 10:42:33

它使我的课程不太灵活/可用。例如:如果没有 Status#to_str,我就无法使用 String#+ 将 Status 输出与其他字符串连接起来。

这是一个糟糕的例子,因为连接字符串是 Ruby 不惯用的做法。字符串插值是首选方式:

a_string = "Output was: #{results}"

正常™,因为字符串插值实际上对插值表达式的结果调用to_s

这似乎违反了鸭子类型的精神。对象的用户(即:将其作为参数获取的方法)不应该关心该对象是什么,而应该只关心它可以做什么。 (在这种情况下,“do”意味着“可以表示为字符串/整数”。)

我认为“可以表示为字符串/整数”并不是真正的行为。 IOW:“对象可以做什么”是关于特定上下文中有趣的行为,而“可以表示为字符串/整数”并不是真正有趣的行为。

例如,如果您说“Status IS-A Integer”(这本质上就是 to_int 的含义),那么您可以用它进行算术运算。但是“将 42 添加到未找到的文件”到底是什么意思呢?成功的对数是多少?失败的平方根是多少?

在上面的字符串插值示例中,有趣的行为是“可以显示”。这基本上是通过实现#to_s来表明的。连接两个字符串,OTOH,需要两个字符串。

“本质上是一个字符串/整数”的论点对我来说非常模糊。例如,您会看到 Float#to_int 被多次提及。故事是这样的,由于浮点数总是有一个整数部分,所以 to_int 是一个有效的方法。然而,我认为这是虚假的:Float不是一个整数(因为它有一个非整数分量),因此尝试等同它们的“类型”没有多大意义。您可以合法地将 Float 转换为整数(通过截断),但是我可以说我也可以将我的 Status 转换为整数(通过“截断”所有非退出代码信息)。

再说一遍,这是一个相当无力的论点,因为我实际上同意你的观点:那是错误的。

在德国法中,我们有一个难以理解且不直观的原则,但我认为它完全适用于这里。它被称为“Keine Gleichheit im Unrecht”(错误中没有平等)。这意味着宪法赋予的平等基本权利仅在法律范围内适用。换句话说:橙汁并不能使谋杀合法化。

因此,仅仅因为 Ruby 核心库中存在垃圾代码(相信我,有很多),并不意味着您也可以编写垃圾代码 :-)

在这种特殊情况下, Float#to_int 完全是错误的,不应该存在。 Float不是Integer的子类型。乍一看,情况恰恰相反,即 Integer#to_float 是有效的,但实际上也不是这样:在 Ruby 中,Integer 具有任意精度,但是Float 具有固定精度。实现 Fixnum#to_float 是有效的,但这将是一个坏主意,因为 Integer 可以神奇地从 Fixnum 转换BigInteger 并返回,因此 #to_float 方法会“神奇地”出现和消失。

最终帮助理解to_xto_xyz之间的区别的是Array#join:它打印出数组的元素,由分隔符对象分隔。它通过对数组的每个元素调用 to_s 和对分隔符调用 to_str 来实现此目的。一旦您了解了为什么它在一个上调用 to_s 并在另一个上调用 to_str,您就基本上准备好了。

(尽管您对Float#to_int的评论已经表明您确实理解。)


旁注:对于代数上下文中的双重分派,Ruby实际上使用#coerce 协议。因此,如果您希望 1 + a_status 示例正常工作,则需要实现 Status#coerce。但是,请不要。

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.

﹏半生如梦愿梦如真 2024-08-17 10:42:33

鸭子类型的精神肯定不会让人查找 Status 对象的源代码来找出返回的内容。

就我个人而言,我认为您应该通过两个实例方法公开文本结果和退出状态:

class Status

  ...

  def message
    @output
  end

  def exit_status
    @status.exitstatus
  end

end

然后按如下方式使用它

a_string = "Output was: " + results.message
a_int    = 1 + results.exit_status

,这对于任何阅读代码的人来说都立即有意义,恕我直言。

The spirit of duck-typing certainly wouldn't have someone look up the source code for the Status object to figure out what is getting returned.

Personally I think you should expose the text results and the exit status through two instance methods:

class Status

  ...

  def message
    @output
  end

  def exit_status
    @status.exitstatus
  end

end

And then use it as follows

a_string = "Output was: " + results.message
a_int    = 1 + results.exit_status

Which immediately makes sense to anyone reading the code, IMHO.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文