From: Austin Ziegler Date: 2003-11-06T15:55:16+09:00 Subject: Re: Managing metadata about attribute types Given: class Weight def initialize(units, amount) @units = units @amount = amount end end class StockItem 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. Fair enough. But what is a Weight? You say that the StockItem expects that the object stored in @weight will behave as a Weight. Do you enforce it, or just let it blow up later if someone disobeys the contract? > 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. Um. You've sort of given the right answer here, I think, for making this generic. There's a conversion table in the *digester*. So what you do is you make your conversion table a bit more rich than it is in Java. Someone -- Rich Kilmer? -- suggested that a #from_string is appropriate. Well, why not? > 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 That's certainly a possibility; but not the one that I'd do, because it requires creating a "to_weight" on both the original class and String. Rather, I might suggest doing: class Weight def self.from_String(weight_string) # code necessary to parse the string into units and amount, # which are then passed into Weight.new end end class StockItem alias_method :__set_weight, :weight= def weight=(param) w = Weight.__send__("from_#{param.class}".intern, param) self.__set_weight(w) rescue self.__set_weight(param) end end You've now got everything you need to be able to handle such a string very easily. You would still be able to do both: stock_item.weight = "0.75 kg" stock_item.weight = Weight.new("kg", 0.75) > Hmm .. libraries don't ever call "freeze" on their classes, do they? Rarely. >> 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? If you're expecting to deal explicitly with the string result, yes. But if you're going to operate on the object and then deal with the string, no. The violation of the contract is a small one -- and, I argue, a common one. I've been bitten more than once by the lack of a .0 ... > 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). No, not really. There's a BigDecimal class that works a bit like the Bignum class. (3**3**3).class # => Bignum (3**3**3).to_i.class # => Bignum Just because #to_i/#to_f is called doesn't mean that an actual Integer or Float will be returned; but if it *isn't* returned, then the class is more or less guaranteeing that it will behave exactly as it should. >> 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. Fair enough, as I said. But don't you, the user, have to create such conversions before running the digester (excepting well-known "common" types)? >> 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. I never said it was mandatory. I have a lot of methods that operate on objects assuming that they are of the correct type (per duck- typing). But on those few methods where I must ensure behaviour, I will do such enforcement. I tend to do failure enforcement (that is, I fail when I encounter something that doesn't match), but I enforce compliance regardless. >> 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. Right. >> 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. Perhaps. It's been an interesting discussion, and I think I've created some useful metacode out of the discussion, for sure. -austin -- austin ziegler * austin@halostatue.ca * Toronto, ON, Canada software designer * pragmatic programmer * 2003.11.06 * 01.55.07