From: Ben Giddings Date: 2003-11-22T04:28:34+09:00 Subject: Interface checking I've been skimming over the thousands of emails about "stereotyping" for the last few days, and I thought I'd step in now and say something. First of all, please Austin and Sean, keep the personal attacks off the list. There's no need for that. You both have strong views, and that's great, but there's no need to expose the rest of us to your personal attacks. (And Sean, that does include complaining about Austin's off-list personal attacks as well). Just keep it professional on the list, please. Now for the actual issue. If I try to compile the following C program, I'll get some compile-time errors: float convert(float temperature) { return (temperature - 32) * 0.6; } int main(int argc, char **argv) { printf("Temp is %d\n", convert(32.0)); printf("Temp is %d\n", convert(32)); printf("Temp is %d\n", convert("hot")); return 0; } It will tell me that "hot" is an incompatible argument to "convert". In this case, that's true. The C compiler does a great job of catching this error. It is helpful because it identifies the function If "convert" were declared in an external library and the documentation of it were somehow inaccessible this is just the sort of information you'd want, before you actually tried to use the program. If you try a brain-dead simple Ruby implementation of the same thing, you get an error, but it isn't nearly as informative: def convert(temp) (temp - 32.0) / 0.6 end puts "Temp is " + convert(32.0).to_s puts "Temp is " + convert(32).to_s puts "Temp is " + convert("hot").to_s foo.rb:2:in `convert': undefined method `-' for "hot":String (NoMethodError) from foo.rb:7 If you use "duck typing", in the sense that you trust whatever is passed to the "convert" method can be converted to a float, you get no errors. On the other hand, the result probably isn't what you want. def convert(temp) (temp.to_f - 32.0) / 0.6 end ... Temp is 0.0 Temp is 0.0 Temp is -53.3333333333333 Things are actually worse if you assume whatever is passed to the program can be converted to farenheit: def convert(temp) (temp.to_farenheit - 32.0) / 0.6 end ... foo.rb:2:in `convert': undefined method `to_farenheit' for 32.0:Float (NoMethodError) from foo.rb:5 The problem I see with all these errors is twofold: 1) There's no way to spot them until you actually call the "convert" method 2) The error message isn't very informative compared to the C error message which tells you the name of the function and that you're passing an inappropriate type. Now, a lot of these problems can be overcome by making a less brain-dead "convert" method, but IMHO that isn't the best strategy. That approach requires the person writing the library method to be able to anticipate all the ways in which someone might misuse the methods they provide, but at the same time, not fail when the method is used in an unexpected way that would actually have worked. Most library writers are either not smart enough (me) or too lazy (me) to do that. I also don't think that testing is the answer either, laziness and difficulty in writing complete tests being the reason there too. Obviously, for Ruby the solution isn't to implement static type checking. The general solution isn't to check the class of the argument inside the method either. In a dynamic language like Ruby that's either too restrictive or prone to error. I don't think checking "respond_to?" is a solution either. Take the case above where I ended up doing "hot".to_f. It responds to that method, but it is unlikely that the person passing "hot" in as an argument wanted it interpreted as 0 farenheit. If we want to get from Ruby what we get from static type checking in C, we need a way to get the two things above: the name of the method that failed and the reason it failed. Consider the error message you get in the first example: foo.rb:2:in `convert': undefined method `-' for "cold":String (NoMethodError) The problem I have with this error message is that it isn't clear that "cold" is the argument to the method, and it isn't clear why exactly it failed. The error message says that '-' is an undefined message, but (at least to me) that doesn't translate to "you passed me a String but I was expecting something like a number". Now maybe it is possible to modify Ruby so that when you define a method, it automatically wraps it in a begin/end block that catches NoMethodErrors and, if it determines that the object that caused the NoMethodError is an argument to the method it modifies the error to say something like: foo.rb:2:in `convert': undefined method `-' for "cold":String; Possibly a an illegal argument to convert? (NoMethodError) What would make things even more interesting is if somehow it were possible to find out what methods an object is expected to implement in the course of a method, so given a method like: def convert(temp) if temp.respond_to?(:to_farenheit) (temp.to_farenheit - 32.0) / 0.6 else (temp.to_f - 32.0) / 0.6 end end It might know that temp needs to implement "respond_to?" and one of "to_farenheit" or "to_f". Then you could get an error message like: foo.rb:2:in `convert': undefined method '-' for "cold":String; Possibly a an illegal argument to convert? Required methods: [:respond_to?, :to_farenheit] or [:respond_to?, :to_f] (NoMethodError) I think that would be incredibly useful, but most likely very, very difficult to implement. Anyhow, there's still the other problem: C finds argument-type errors at compile time, Ruby finds them at runtime. Is there a way to have Ruby find them immediately rather than when the method is called? Because of the dynamic nature of Ruby, the only possible answer to this is 'no'. But, having said that, there are other possibilities. When you run ruby with the '-d' flag set, it prints to stderr all exceptions, whether caught or not. This means that it often prints things to stderr that don't need to be there. Maybe it is possible to come up with a similar type checking mechanism. At "compile time" (whatever that means in an interpreted language) it would try to look for type inconsistencies, meaning arguments to methods that don't implement the required methods. It could print those out as warnings, then continue, hoping that by the time it actually calls those methods, things will have been resolved. How this would be done? I have *no* idea. It sure sounds difficult, but it would sure be useful. Now even of all the above could be done, there would still be errors. Just because a method exists doesn't mean it does the right thing. A method named 'read' might be a verb telling an IO-type object to read something, or it might represent a boolean flag indicating a message-type object has been read. Maybe the easy solution for the short term is to do like Python and emacs-lisp: def foo(bar) "Foo takes a 'Bar' as a parameter. It will try to do X, Y and Z with it" ... end irb> foo("hot") in `convert': undefined method `-' for "hot":String (NoMethodError) irb> foo.docstring "Foo takes a 'Bar' as a parameter. It will try to do X, Y and Z with it" irb> foo(25.0) 5.0 irb> This docstring type functionality wouldn't help much when running programs on the commandline, but it would be really useful when debugging or poking at things with irb. Comments? Ben