From: Robert Klemme Date: 2011-04-08T19:12:16+09:00 Subject: Re: Hash Surprises with Fixnum, #hash, and #eql? On Fri, Apr 8, 2011 at 9:30 AM, Clifford Heath wrote: > On 04/07/11 19:19, Robert Klemme wrote: >> >> On Thu, Apr 7, 2011 at 6:05 AM, Clifford Heath  wrote: >> I also doubt whether it is a good idea to allow for subclassing of an >> integer like class.  What use case do you have in mind which would >> make this necessary? > > What's wrong with the case you use in that blog post? You mean, make HexNum a subclass of Integer? Yes, actually that's what I had attempted at the time but failed for technical reasons (explained in the blog). As it turns out it's generally not necessary to inherit Integer in Ruby to create a class which behaves like an integer (most of the time). > But as it turns out, > I'm implementing a fact-based modeling DSL, where it's sensible to have > classes like "AgeInYears" being a subclass of an integer like class. > The formalism for this comes directly from sorted first-order logic, > which makes a good deal more sense than the broken O-O paradigm discussed > elsewhere in this thread. > > I suspect that you "doubt it is a good idea" only because Ruby's object > model for numbers is inconsistent, and you're defensive about that. Where exactly do you see the inconsistency? I can see that a few things in that area do not match common expectations. But I don't think it's really inconsistent. Your problem is not so much with numeric classes IMHO but rather with implementations of class Hash in different versions of Ruby. Namely do they have issues treating instances from different class as equivalent. > Note that I'm not actually subclassing any core integer class. I'm just > defining a new base class "Int" which contains an integer, and so far > as is possible, acts like one, including being found in a Hash using a > Fixnum/Bignum key. > > If Fixnum and Bignum can act like Integer subclasses, why can't my class? Fixnum and Bignum do not share common values so you never have instances of different classes representing the same numeric integer value: irb(main):003:0> (1<<100).class => Bignum irb(main):004:0> (1<<100)>>99 => 2 irb(main):005:0> ((1<<100)>>99).class => Fixnum So that situation is a bit different. >>> In particular, the Hash implementations work (and break!) differently in >>> MRI, Rubinius and JRuby. It's documented to use only #hash and #eql?, >>> but that's not always true (sometimes these have hard-wired >>> optimsations). >> >> When you violate contracts you cannot expect code to work properly. > > I have not violated that (unstated!) contract. Read again; I redefine > Fixnum#eql? as self.orig_eql?(i.to_i) - the to_i makes it symmetrical. > (Debate the wisdom if you wish, it's just for demonstration purposes.) Yes, you're right. I probably mixed in a discussion about equals() in Java needing to test for the same class (and not instanceof) to achieve real equivalence. At least we had a nice discussion about OO and inheritance because of that. :-) > However the Ruby interpreters do not honor that. In short, *all three* > mentioned Ruby interpreters violate the Hash contract, which states that > hash and eql? are used for Hash lookups. Not just sometimes, but all the > time, including for integers. Apparently there are optimizations done under the hood (similarly to duping an unfrozen String as key) which is probably OK from a pragmatic point of view (what you attempt seems rather seldom done). > The Ruby interpreters should behave the way the Hash documentation says they > do. Well, they do - most of the time. :-) > The Ruby documentation should explicitly state that eql? must be defined > symmetrically, or should require that the Hash implementation uses it only > in a known direction or both. Right, there is certainly room for improvement. >>> The Hash documentation does not say whether #eql? will be called only on >>> items in the hash, or only on keys being used to probe the hash. It >>> should >>> be one or the other, since a.eql?(b) might not always mean b.eql?(a). >> >> But that is the contract as far as I can see. > > That's not documented anywhere I can see. Certainly not in TRPL, see > sections > 3.4.2 on page 68, and section 3.8.5.3 page 77. It makes sense, but it's not > stated. Right again. Maybe the requirement can be inferred from other properties but it would certainly make sense to stress it. >>  Having different >> results for both violates the equivalence relation which means all >> bets are off. > > No. I can fix the asymmetry. I can't make the interpreters honor that fix. But since you already embarked in monkey patching core classes you can easily extend that a bit to Hash#[] and Hash#fetch. That should work on all platforms. Another possible remedy would be to wrap Hash instances in something else which adds logic to [] and fetch() to convert types if necessary. >>> You'll see that the behaviour is very unpredictable. >> >> Yes, because of your violation of the contract. > > No. Because the Ruby interpreters don't honor the Hash contract. As said, they do it most of the time. You introduced a corner case here by fiddling with a core class which is known to lead into deep water. Treating instances from different classes equivalent does work for other classes: 12:07:22 Temp$ allruby ha.rb CYGWIN_NT-5.1 padrklemme2 1.7.9(0.237/5/3) 2011-03-29 10:10 i686 Cygwin ======================================== ruby 1.8.7 (2008-08-11 patchlevel 72) [i386-cygwin] [[#, 3, 1], [#, 5, 2], [1, 3, nil], [2, 5, nil]] ======================================== ruby 1.9.2p180 (2011-02-18 revision 30909) [i386-cygwin] [[#, 129497438, 1], [#, 602294680, 2], [1, 129497438, nil], [2, 602294680, nil]] ======================================== jruby 1.6.0 (ruby 1.8.7 patchlevel 330) (2011-03-15 f3b6154) (Java HotSpot(TM) Client VM 1.6.0_24) [Windows XP-x86-java] [[#, 1, 1], [#, 2, 2], [1, 1, nil], [2, 2, nil]] ======================================== jruby 1.6.0 (ruby 1.9.2 patchlevel 136) (2011-03-15 f3b6154) (Java HotSpot(TM) Client VM 1.6.0_24) [Windows XP-x86-java] [[#, 1, 1], [#, 2, 2], [1, 1, nil], [2, 2, nil]] ======================================== 12:07:36 Temp$ cat -n ha.rb 1 2 require 'pp' 3 4 A, B = 2.times.map do 5 Class.new do 6 def initialize(x) 7 @v = x.to_i 8 end 9 10 def to_i 11 @v 12 end 13 14 def hash 15 @v.hash 16 end 17 18 def eql? o 19 case o 20 when A, B 21 @v == o.to_i 22 end 23 end 24 25 alias == eql? 26 end 27 end 28 29 h = {A.new(1) => 1, B.new(2) => 2} 30 31 keys = [B.new(1), A.new(2), 1, 2] 32 33 pp keys.map {|k| [k, k.hash, h[k]]} 34 35 12:07:40 Temp$ > Please try to read more carefully. Will do. Cheers robert -- remember.guy do |as, often| as.you_can - without end http://blog.rubybestpractices.com/