Damian’s modules are the ultimate in user-friendliness, or to put it another way, DWIMminess (Do-What-I-Meaniness). He has relatively few functions, each of which is very full featured. Each takes a variety of kinds of arguments, and interprets what you probably wanted to do with that kind of data. They’re very “smart”, in a sense. And you could imagine that this would enable a very easy programming style, because the module is interpreting what you want to do along the way.
Another example of DWIMminess is the entire Perl 6 language. Well, it’s DWIM factor has been decreasing recently, but usually features are introduced as though they were in a Damian module and then slowly simmered into something a little stupider (describing the behavior of the functions, not the decision to make them behave that way). Why do we do that to Perl 6? Why might Damianesque interfaces not be as nice for programmers as it would seem?
First, let me make a case for “smart” interfaces. I want my command line to Do What I Mean as much as possible. I use this nice little program on “my taskbar called Deskbar, in which I can type a variety of things and it uses heuristics to determine what I wanted. For example, if I type “mplayer ~/movie.avi”, it will open mplayer and play that movie. If I type “gmail.com”, it will open my web browser to Google mail. If I type “dict persephone”, it will open a terminal with the definition of “persephone”. And it is wonderful; I don’t think it has ever done the wrong thing.
My case is that that is not a good thing to do when programming larger applications. Many folks would agree with me, but that doesn’t matter. I’ll bet they don’t know why they agree with me.
It’s about the information available to the programmer. When we’re writing code, usually we want to be as general as possible—to make as few assumptions as possible—when we’re programming part of a large application. We want to be able to work on as many forms of data as possible, and be agnostic to it if we can. For example, an object which maps strings to Foos doesn’t need to know anything about Foos to do that, so you could reuse that implementation for an object which maps strings to anythings.
On the other hand, when we’re on the command line, we know everything. We know what command we’re running, which arguments we’re passing, what error message it told us when we tried the same thing thirty seconds ago, whether the files we’re passing exist, whether the arguments we’re passing have spaces in them, etc. etc. We can quickly detect a failure, and we can also verify that it looks right pretty quicky. If you type “open -f http://google.com”, you don’t expect it to delete your hard drive. This opens something somehow, and it’s going to operate primarily on http://google.com. Who knows what -f means… it doesn’t really matter, since you can probably see what it means pretty quickly. But if you typed “$cmd -f $arg”, you can’t see what -f means pretty quickly, because it means something different for each value of $cmd. So even after you think your code is sturdy, some $cmd may come along someday and cause something totally unexpected to happen.
My stance is that DWIMminess is good in principle. More programming languages should do it. But you have to be very careful when you are dealing with complex programming problems. Essentially, the “I” in “DWIM” is what you need to worry about. “I” is the person who wrote the code: whatever it looks like to them, that’s what it should do. And that explains my all-too-common position in Perl 6 arguments of “DWIM ONLY AT COMPILE TIME!”. At compile time, the decision about what to do is made based only on information the programmer has, so if it’s a good DWIMmer, it will make the same decision the programmer would have made. Don’t choose behavior based on what the runtime values are, since that can cause two different interpretations of what IM in the same lexical position, i.e. your DWIM thinks that the programmer meant two different things by the same line of code. While that is an interesting literary device, I don’t think I’ll face much resistance convincing you that that’s a bad thing in programming.
Keep in mind, this is neither a case against polymorphism nor is polymorphism (including multimethods) a case against this. I am a big fan of strongly-typed polymorphism, like C++’s and Java’s1. If you write a->add(b), you don’t have to know what exactly a is, but you do have to know whether it’s a number or whether it’s a set. The word “add” means pretty different things on numbers and sets, and the programmer should know which one he’s talking about. Thus, in order to call add, a must be declared as something that has an add method, say “Set”. Then when something derives from “Set” and defines its own “add”, it knows that it is talking about adding an object to a collection, not adding a number to another, so the programmer’s original intent by a->add(b) is preserved.
1C++ and Java were not examples of languages that got DWIM right, they were just the most common example of strongly typed polymorphism. C++ and Java both lack another very important feature of abstraction: the ability to ignore what isn’t important. Haskell, which I didn’t want to mention for fear of sounding like a zealot, has strongly typed polymorphism and the ability to ignore what isn’t important (called parametric polymorphism): it puts “What I Mean” in the naming of the methods; i.e. you can’t have add both mean adding numbers and adding to collections, their types would be incompatible. So you’d have to have add and addToCollection or something, which is why the programmer can leave out the types and still be perfectly clear about what he means. Dynamically typed languages like Perl and Python have the ability to ignore what isn’t important, but give up the ability to say exactly what you mean all the time. Some runtime data may come into your function one day and break it, by trying to add numbers when you meant add to a collection. The method case doesn’t break much, because good method naming and a little bit of argument arity checking will quickly find your problem for you when you run your test suite. Operators are a different story, though: if you overload operators too heavily (which is done at runtime in those languages), it can do the wrong thing and you won’t catch it until 1,000 lines later. That’s because operators have a fixed arity, and many object types will overload them, so where is it going to die?