From: Austin Ziegler Date: 2003-11-06T12:27:43+09:00 Subject: Re: Managing metadata about attribute types On Wed, 5 Nov 2003 18:08:04 +0900, Ryan Pavlik wrote: > Austin Ziegler wrote: > >> It isn't inferior, and you don't need type info. Remember -- an >> object should validate or transform its own data. > > I'm trying to stay out of this because I mostly disagree. This however > warrants addressing. > > This is demonstrably wrong. Actually, it's demonstrably correct. It's exactly to the point and perfectly accurate regarding how one should deal with data in a dynamically typed language such as Ruby. In Text::Format, I have a method #hyphenator= which accepts any object that responds to #hyphenate_to with an arity of 2 or 3. I explicitly reject any other object. In documentation, I make it clear that #hyphenate_to should return an array of two objects. In this way, I don't care what *class* an object is, I just care that it's type is a hyphenator (as defined above). > An object cannot validate and transform its own data in this > context in any reasonably general manner. Your StrongTyping module doesn't help with this, either, Ryan. It's not a conversion module. > It's simple when you're addressing a few basic types... String, > Float, Integer, Hash and Array. Not really. If I were to do the following: class Foo def foo=(x) @foo = x.to_f end end Foo.new.foo = [4, 5] I'd get an error. That much is clear. I can program defensively in Foo#foo= by catching the case where x does not respond to #to_f (either as an exception or by #respond_to?(:to_f)), but I'll still want to raise an error. But your StrongTyping module doesn't help here, either: class Foo define foo=(x) expect(x, Numeric) @foo = x end end Whee. You've semi-automated the error checking. Rather than attempting to convert x to a float, you require that it be a numeric. Great. Foo#foo= still is expected to work as a float, but if you're given a Fixnum, you won't get the expected results necessarily. Even in your "academic" example on the StrongTyping page falls down with a bit of defensive programming: def sendMsg(bridge, n, m) sleep(n) bridge.open sleep(m) ensure bridge.close end If you know you're going to be doing something that could leave things in an intermediate state, ensure that they don't get left in such a state. The bridge is closed when you pass "m" as a string. > This isn't general, though. What if I want a Foo, and you give me > a Bar? Foo was from one module (which shouldn't know about Bar), > and Bar was from another module (which shouldn't know about Foo). > There is there no #to_foo (which may be fortunate depending on > your culinary preferences). This leaves us with only a few > options: > * We just don't allow it. This does no good for us. But this is *exactly* what StrongTyping does, Ryan. It doesn't allow types that it doesn't know how to deal with. > * We convert through an intermediary type. This is inefficient and > may lose data or not work, either. > * We decide the approach is wrong and do something else. > Using #to_* methods are the ruby equivalent of type casting. The > point in this case is not to _convert_ types, it's to provide the > right type in the first place. Instead of giving the attribute a > string and expecting it to be parsed, we want to create the > desired type and hand that off. Oh, bollocks. Item's @cost is expected to work as a float. Not an integer, a float. If I want to ensure that it works as a float, Item#cost= should attempt to *make* the provided parameter into a float. If someone is going to pass me something that can't be converted to what I expect, they're going to get an error. If I'm expecting a Foo, though, then I should probably do something like: def foo=(x) @x = Foo(x) end That's *if* a Foo can be converted from other types. Or, maybe, I'm just looking for a particular method call, in which case I can defensively program as I did with Text::Format. Can it still be bitten by someone who accepts the parameters in a different order or expects different things? Sure. But that's not exactly something that I can program against in any case ... unless I'm using strong typing, and IMO that does the wrong thing. Note that I've done much the same thing with Ruwiki recently -- I've abstracted out what a Ruwiki::Wiki::Token needs to know into a Handler. It responds to certain methods. As the needs for what a Wiki Token needs to know increase, the Handler will increase as well. I'm not even checking to see if the token handler is a specific class. I just expect that it will respond to those methods. As Rich Kilmer said, are you checking for behaviour or namespace? > It has nothing to do with the #attr= function. Strict type > checking at that point is merely a convenience. It's all about > getting the input into a useful format without writing n^2 > functions (for n classes). This is the primary reason I wrote > StrongTyping in fact; the strict checking has merely helped with > debugging a whole lot. It has *everything* to do with the #attr= function in the case given. The OP is dealing with an XML document, which means that everything is a string and #to_f works. The OP has a library of objects where certain things are expected to be floats -- but nothing is done to ensure that. It could be done through StrongTyping, or it could be done through the proc-based attr_accessor that I posted. Or, the two could be combined: attr_accessor proc { |x| expect(x, [Numeric, String]); x.to_f }, :cost or attr_accessor proc { |x| expect(x, Float); x }, :cost The problem here is ultimately that your objects have to know what they expect and how they are expected to be used *in general*. If you've got Item#cost, you can expect that it will be used in mathematic operations. You'll probably do such operations yourself. So, why not do some sort of conversion on the data to make it into what you expect it will be when you're defining your attribute accessor? A lot of people coming from strict typing backgrounds forget that this is done implicitly by the compiler ... or rather, because the type is strictly specified it isn't necessary to do such conversions. They're done automatically when the types can be converted. -austin P.S. It was suggested that: attr_accessor proc { |x| x.to_f }, :cost is overkill. Thus, the version I attached last night includes: attr_accessor [:cost] => :to_f I may add other enhancements to that mechanism (as providing multiple method symbols in an array and chaining them). -- austin ziegler * austin@halostatue.ca * Toronto, ON, Canada software designer * pragmatic programmer * 2003.11.05 * 10.54.20