[lnkForumImage]
TotalShareware - Download Free Software

Confronta i prezzi di migliaia di prodotti.
Asp Forum
 Home | Login | Register | Search 


 

Forums >

comp.lang.ruby

Re: Managing metadata about attribute types

Austin Ziegler

11/6/2003 6:55:00 AM

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

<stock-item name="spanner" cost="12.50" weight="0.75 kg"/>

> Now as a programmer dealing with the above problem, I want to be
> able to tell xmldigester than when it encounters the <stock-item>
> 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)

<snip>

> 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