[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 3:28:00 AM

On Thu, 6 Nov 2003 10:16:41 +0900, Simon Kitching wrote:
<snip>
> Regarding whether the target class should be responsible for
> accepting a string and doing the conversion... I think it is
> definitely *not* the receiving classes' responsibility to do the
> conversion.

It's not simply a matter of conversion from a String, as I'll
demonstrate below.

> Here's my original class, with the initial implicit assumptions
> spelled out more clearly as comments.
>
> class StockItem
> # contract with user: any value assigned to name must act
> # like a string
> attr_accessor :name
>
> # contract with user: any value assigned to cost must act
> # like a float object.
> attr_accessor :cost
> end

Let's test that assumption.

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.

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.)

In a statically typed language, conversions like this can be made
implicit because the types themselves are explicit. The Java version
*will always* be dealt with as if it were a float ... because it
always *will* be a float.

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.

> Isn't this a valid API for a class to provide? As far as the author of
> StockItem is concerned, cost is a float.

I disagree. If you want to treat the attribute as a float, then it's
your responsibility to ensure that it *is* a float. Otherwise,
you'll get unexpected results when someone doesn't *quite* respect
the API/contract.

[...]

> Not to mention that writing those "conversion" methods by hand is
> ugly.

Well, they can be. That's why I wrote the extension that I did.

>> You're right, they shouldn't. But if your warehouse management
>> classes don't do what they can to ensure their data integrity,
>> then there's a problem with the classes -- not with the XML
>> library. I'm not trying to be difficult here; just pointing out
>> that I think you're trying to fix the problem from the wrong end.
> The StockItem's contract clearly states that it only accepts Float
> types for the cost attribute. It doesn't actually need to enforce
> its data integrity - it is the calling code's responsibility to
> use StockItem correctly.

Well, yes, the documented contract is violated ... but there's no
programmatic contract. IMO, defensive programming suggests that if
you need something to behave a particular way, you do what you can
to ensure it.

>> 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.

[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.

> As you can see, I'm not interested in "type strictness" at all.
> What I need is simply "what type of object should I generate in
> order to be able to validly assign to cost without violating the
> API contract of the StockItem class"...

Maybe there's a place here for an enhanced version of #coerce.

> Changing the StockItem class contract is one solution, but that
> screws up all other code that really depended on the original
> contract being valid.

No, it doesn't. Doing a #to_f doesn't change the original contract.

> Oh, and what if the target attribute is a "Date" class, and I want
> to globally control the way string-> date mapping works? If it is
> distributed across every class that has a Date attribute that is
> much trickier to handle than if I somehow know that classes X, Y
> and Z have date attributes and the xmldigester code does the
> string-> date conversions before the assignment.

Why would you want to globally control it? The parsedate routine
(don't quite remember where it sits) handles this.

> The thread about namespaces still has me pondering a little. I'm
> not sure it's relevant to my issue, though, is it?

It's an offshoot of StrongTyping. When you do a #kind_of? test, you
are doing something of a namespace test.

-austin
--
austin ziegler * austin@halostatue.ca * Toronto, ON, Canada
software designer * pragmatic programmer * 2003.11.05
* 22.26.52



1 Answer

Simon Kitching

11/6/2003 5:34:00 AM

0

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

<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.

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