Simon Kitching
11/6/2003 5:34:00 AM
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