From: djd15@... Date: 2003-08-09T04:58:30+09:00 Subject: Re: Ducktype, right? ----- Original Message ----- From: Ben Giddings Date: Friday, August 8, 2003 3:14 pm Subject: Re: Ducktype, right? > On Fri August 8 2003 2:36 pm, Chris Morris wrote: > > Ben Giddings wrote: > > I agree with Ryan's reply to you. If I only need the one method > on the > > object I'm interacting with -- should that one call not be > allowed > > simply because all of the other methods that I don't need aren't > > consistent with what it *should* be? > > Ouch, this is one of the most difficult-to-understand sentences > I've seen in a > long time. > > I'll pretend I understand it and give an example: > > > # > # Get the contents of the object, if it acts like a string, > # simply return it, if it acts like a file, return its contents, > # if it acts like a frobnitz, foozle it. > # > def getContents(obj) > if obj.respond_to? :read > return obj.read > elsif obj.respond_to? :to_str > return obj.to_str > elsif obj.respond_to? :foozle > return obj.foozle > end > end > > class EmailMessageBody < String > > attr_accessor :read > > def initialize > @read = false # By default, mark the message body as unread > end > end > > Imagine that the "getContents" method is buried deeply within the > source code > to a library somewhere. The programmer would never see that > method, even if > he/she did see the commenting of the method. Now what happens > when I pass an > EmailMessageBody into getContents? It will see that it responds > to "read" > and return a boolean. That's not what you want. That's not what > the > documentation says, but that's what it will do. This could be a > really hard > bug to track down. > > Testing to see if the "read" method is supported is not really > "duck typing", > in the sense that you're not really checking to see if it's a > duck. Just > because the object implements a certain method doesn't mean that > that method > does what you expect. > > In certain cases, checking to see if a method exists is enough. > The method > "to_str" is a prime example. If someone implements that method in > a way that > doesn't return a string, they deserve whatever problems they get. > There are > plenty of examples of how to_str is used everywhere else, and the > function > name is pretty descriptive. Other functions are less obvious. > > Somebody else gave this example (though I added the "implements > Duck" that I > think was just accidentally left out): > > interface Duck > { > void quack(); > } > > class MyDuck implements Duck > { > void quack() > { > System.exit(); > } > } > > Here the "implements Duck" is essentially an agreement saying "I > want other > peole to see this as a Duck". You're free to break that > agreement, but if > you do, you can expect trouble. > > On the other hand: > > class Psychiatrist > { > void quack() > { > quack_references++; > } > void shrink() > { > shrink_references++; > } > } > > Just because someone has a "quack" function/method, doesn't mean > they want > their object to be treated as a Duck. But if you have a Psychiatrist, why would you pass it to a method that expects a Duck? The method should be documented as expecting a duck, so passing a Psychiatrist is wrong. The only difference between Java and Ruby in this respect is that in Ruby, you'd find out about it at runtime, which is the best you can do anyway, since implements_interface? is a method call anyway (granted, it's easy to figure out than having quack() do something wrong). > When you create a class and > add > methods to it, should you always have to say: "Hmm, what's the > most popular > way _____ is used", and make sure yours acts the same way? I think that generally you design a class for a specific purpose, and only pass it to methods that expect it to act in the way it was designed. > My understanding was always that "implements Foo" in Java meant: > "my class > will define the following methods, in the way that Foo describes, > so you can > treat my class as a Foo". > > As for the subject of why it is useful to implement a set of > methods rather > than just the one you care about, consider a function like: > > def dumpContents(obj) > if obj.respond_to? :write > obj.write(data) > obj.flush if obj.respond_to? :flush > obj.close if obj.respond_to? :close > elsif ... > end > end > > I think it would be much more clear if you could simply say: > > def dumpContents(obj) > if obj.implements_interface? :WriteableIO > obj.write(data) > obj.flush > obj.close > elsif ... > end > end Ruby is certainly more verbose in its checking of type, when looked at this way. Notice in your above example, the second version isn't the same as the first. The first version accepts something that allows writing, but not necessarily flush and close, while the second version only accepts something that does all three. What if you want to instead output to a string? Flush and close don't make sense in that case. So you'd end up with an interface for each, which would look like: def dumpContent(obj) if obj.implements_interface? :Writable obj.write(data) if obj.implements_interface? :Flushable obj.flush end if obj.implements_interface? :Closable obj.close end end end Which could be shortened, but it's almost no better than duck typing. The only thing that you gain is that implementing Flushable implies that flush has certain semantic meaning, which needn't be the case, and in any case can be solved by documenting obj. If you don't want to do it that way, you'd need to have separate cases for WritableIO and StringIO which involves code duplication, so it's not ideal either. Furthermore, what if you want a one-shot object? Take the idea of anonymous inner classes in Java. You don't want to define a whole listener class just to handle System.exit on window closing, so you define a one-shot anonymous class like this: addWindowListener(new WindowAdapter() { public void windowClosed(WindowEvent e) { System.exit(0); } }); You could do this in ruby as follows: obj = Object.new def obj.windowClosed(e) exit(0) # I'm not sure if this is the right method, but you get the idea end addWindowListener(obj) Now, there's no way to make obj extend a particular interface other than using WindowListener.new as the base instantiation. However, consider that you could have something like: exitObj = Object.new def exitObj.windowClosed(e) exit(0) end def exitObj.actionPerformed(e) exit(0) end ... addWindowListener(exitObj) closeButton.addActionListener(exitObj) (this is all very Swing/AWT style) So you could use the same object for all your close operations (or all your open file operations, etc.). However, if you used interfaces, exitObj would have to implement both WindowListener and ActionListener, and there's no way to make an anonymous inner class that does that (I think), so you'd need to define a real class. In ruby it's worse, because it would require multiple inheritance, since it'd have to extend both WindowListener and ActionListener, as there are no interfaces, per-se in Ruby. If I may comment on your getContents example, please take no offense, it seems a bit too general an example for me to see it as a real problem. It tests several dubiously related methods and and returns their results. It doesn't seem like something that would be in real code. It reminds me of a discussion I read on here from, say, a year ago. Someone was talking about how in ruby, because you can dynamically change methods, you can never know whether your code will work or not. Their point was that someone could re-define how a method works and break a bunch of your code. The question is, why would someone do that? And even if they did, it would likely happen in testing, and would produce errors. Unless you're reading in user input and eval()ing it, you won't have this problem, as long as you document and test. I can't fully answer your getContents method because it lacks context. Something above getContents would have to specify how objects passed in should behave in documentation, and if someone doesn't follow that, then it breaks. Java interfaces recommend how the implementation of their methods behave. That needs to be recommended in Ruby somehow, whether in documentation or in some other way. read() could have two different meanings in two different modules, so you shouldn't pass an object from the first to a function from the second. Anyhow, I'm rambling on too much, so I'll stop here and possibly resume later. - Dan