From: Simon Kitching Date: 2003-11-06T14:33:40+09:00 Subject: Re: Managing metadata about attribute types On Thu, 2003-11-06 at 16:27, Austin Ziegler wrote: On Thu, 6 Nov 2003 10:16:41 +0900, Simon Kitching wrote: > I hope you realise that the StockItem was just an example I made up out of thin air for the purposes of the discussion - it isn't a real class in use anywhere. Xmldigester is like xml-config; it is a library to configure any set of objects. The StockItem class is just one example. Here's a slightly more complex example, that might get away from the triviality of the StockItem's cost attribute example. class Weight def initialize(units, amount) @units = units @amount = amount end # other weight-related methods here, with defined behaviours # and associated contracts.... end class StockItem # user contract: anything assigned to this attribute must behave # like a Weight object. attr_accessor :weight attr_accessor :name attr_accessor :cost # Float end Now as a programmer dealing with the above problem, I want to be able to tell xmldigester than when it encounters the tag it is to create a StockItem instance, then create a Weight instance and initialise it appropriately from a string, then assign that initialised object to the StockItem's weight attribute. The Java Digester version does this automatically, by determining that the StockItem has a "weight" attribute of type "Weight", and that there is a "weight" xml attribute (with a string value). It then invokes a table of data-conversion methods to convert the string to the target type; built-in types are already in the table, and user-specific types (like Weight) can be added as needed. You're suggesting that in the file which contains the "xml-parsing" code, I re-open the existing StockItem class, and use some approach like the attr_accessor modification to "wrap" the existing weight= code, resulting in something that effectively works like: class StockItem # reopen existing class alias :__weight= :weight= def weight=(param) weight2 = param.to_weight __weight = weight2 end end and Weight doesn't have a to_weight method yet, so I'd need to reopen that class too: class Weight # reopen existing class def to_weight return self end end and finally add a method to String: class String # reopen string def to_weight # some code to instantiate a Weight object and # initialise it from the String's value end end Yes, I am now able to do this, which would indeed satisfy my requirements: # can now assign via strings stock_item.weight = '0.75 kg' and # can still use StockItem instances as per normal stock_item.weight = Weight.new('g', 750) It seems a lot of work, though. And I'm not sure I'm too fond of adding methods to the String class. Nor of the overhead that now exists on every assignment to stock_item.weight, though that's not so important. And what about if Weight is actually Acme::Warehouse::Weight? I'll give this approach some serious thought, though. Hmm .. libraries don't ever call "freeze" on their classes, do they? > s = StockItem.new > s.name = "Apple Pie" # An apple pie... > s.cost = 10 # Costs $10... > per_slice = s.cost / 8 # Split it eight ways... > puts per_slice # => 1 > > Therefore, by simply *assuming* that you're getting an object that > can act like a float, you've introduced a huge error. Should I have > entered 10.0 as the price, or divided by 8.0? Either of those would > have guaranteed me a Float context in which type coercion will be > used to ensure a Float result. If, however, we had converted cost to > a float explicitly during assignment, this wouldn't even be an > issue. Without talking about Strings, we've already run into a > problem with StockItem's assumption of Float-ness. Yes, but as you noted yourself, you've violated the contract on the class. I haven't bothered to clutter my example with "defensive programming". I grant that you may be right that the cost method should call to_f on its parameter, so that Integers can also be passed. And maybe every method expecting a String parameter should call to_s on its parameter? Oh, unless nil is acceptable as a parameter, in which case it would be better to do: @name = name.to_s if name I wonder how many of the Ruby standard libraries do this? Certainly no attribute declared with "attr_reader" etc does. Hmm .. by the way, aren't you arguing against duck-typing here? If someone deliberately creates a type which is *like* a Float, then this code would force it to be a real float (assuming that to_f returns a real Float object). > > Compare the same Java: > > class StockItem { > String name; > float cost; > > void setName(String n) { name = n; } > void setCost(float c) { cost = c; } > > String getName() { return name; } > float getCost() { return cost; } > } > > In Java, it doesn't matter if you pass an int to setCost because the > compiler has already marked that as a float -- and it will do an > implicit conversion from int to float. (IIRC, that *won't* work in > Ada, which disallows implicit conversions.) Yes, but for the Java Digester library, implicit conversions are irrelevant. It doesn't try to pass a String to the setCost method and hope the compiler will insert the correct conversion code (it won't anyway). Instead, as described earlier, a table of "type conversion operations" is used to map from the input String to the target type, allowing any target type (such as Weight) to be correctly dealt with. > > The author of the StockItem class *should* have considered that any > numeric value could have been assigned -- and that integer math > wouldn't be a good idea. As above, I agree that for safety cost= could try to ensure the parameter passed to it complies with the contract. I still don't believe that it is mandatory for methods to enforce their contracts, though...user beware should be the motto, for performance and simplicity. > >> attr_accessor proc { |x| x.to_i }, :item_id > > That's some very cool code. I can feel my brain expanding just by > > looking at it! However I don't feel it does what I want, because > > this code actually changes the API of the target class, breaking > > all other code that accesses that same attribute thereafter. > > Actually, it doesn't change the API at all. It enforces the > documented constraints. It's the difference between early and late > detection. I didn't initially realise that if a real Float was passed to this method, then calling to_f on it is ok; it just returns the same object. > > [snip bean info stuff] > > I donno. That still doesn't feel very "Ruby" to me, and I personally > find both StrongTyping and MetaTag clunky, trying to solve things > that I'm not sure are best solved that way. Yes, I'm still worried that there is some obvious rubyish way to handle this. Maybe when I actually issue 0.1 of xmldigester (with whatever API), then start trying to build some apps with it some epiphany will happen. Cheers, Simon