|Keep It Simple, Stupid|
Why breaking can() is acceptableby tilly (Archbishop)
|on Apr 06, 2004 at 01:55 UTC||Need Help??|
This is a personal opinion that chromatic and I have a long-standing disagreement about. I keep on not promoting it to a root node because I don't think that I have time to discuss it right, but since he keeps tweaking, I'll give in.
The original discussion started at Re: Re: Re: Symbol table globbing from object refs. It's been referred to a number of times, most recently at Re: Re: Re: Re: Testaholics Anonymous (or how I learned to stop worrying and love Test::More).
The problem is that when you implement methods with AUTOLOAD, then the default implementation in UNIVERSAL::can can't tell whether or not you have implemented that method. The issue is what you should do about it. chromatic's opinion seems to be that if you implement methods with AUTOLOAD, you should also leave function stubs so that UNIVERSAL::can can tell that those functions really are implemented. My opinion is that mixing AUTOLOAD and UNIVERSAL::can is fundamentally broken, should be treated as fundamentally broken, and we should just accept that as an inevitable fact of life. Only one person gets to be clever at a time, and the guy who wants to AUTOLOAD, wins.
I'll try to explain my views, and I assume that chromatic will be along in a bit to explain his.
My fundamental issue is that that which is not automated, should not be trusted to be reliable. Particularly if the not automated part has any complex or subtle issue. In other words, the fact that we have to ask everyone to be vigilant, means that people will mess up. How do we then handle those failures?
One approach is to complain. Tell people, You messed up! You didn't take care of this issue! Now nagging never seemed very fun to me, nor does it seem very effective. My personal belief is that either you can convince people to align their goals to yours, or else you have to accept that they aren't likely to do what you want. If you try to push them to do what they have no real reason to want, then expect pushback. (No matter how laudable your goals are, or how much people agree with them in theory.)
So what is the issue? It is that if you implement stuff dynamically (and naively) with AUTOLOAD, and UNIVERSAL::can breaks. Do most people care about UNIVERSAL::can breaking? Not really. In fact most people don't use it. So we are asking people to do extra work for something that they likely don't care about. Which never bodes well for success.
Now let's investigate the nature of that extra work. At the very least we are asking for duplication of information. (The same information to be found in AUTOLOAD is done again in declarations for UNIVERSAL::can.) This kind of duplication is exactly the same as duplications between comments and code. Any experienced, competent programmer should immediately distrust that kind of duplication as being inherently unreliable.
But it gets better. If I've gone to the extent of doing an AUTOLOAD, then I either am (or think that I am) doing something fairly dynamic. Such as keeping code in a database (something that I advise against for different reasons), in random plugins, or even having behaviour that is customized per object. (Yes, I've seen AUTOLOAD used for each of these purposes.) In all of these cases, the code with the AUTOLOAD both does not, and should not know what methods the AUTOLOAD will or will not succeed in implementing. How, then, are we to satisfy the UNIVERSAL::can Nazis and declare the right set of methods? (Particularly when that set may differ depending what object tripped the missing method.) The only approach that I see is to override UNIVERSAL::can, which course opens up its own can of worms.
But wait! I'm not done! The basic nature of AUTOLOAD is that the first AUTOLOAD discovered after the lookup failed gets to handle it. So suppose that we have classes A, B, and C, each of which inherits from the previous one. Class A implements an AUTOLOAD, which predeclares as chromatic wants. Class B does the same. Now what happens if someone calls can on something in class C that is implemented in A but not in B? Well the can will say that it is implemented (it finds the stub in A), but when B::AUTOLOAD is called, it has no straightforward way to decide to politely decline the call and let Perl continue searching for another AUTOLOAD. But, you say, we can just cleverly call SUPER::$method and THAT will find the right AUTOLOAD! Except that we are just moving the breakage, SUPER:: does not play well with multiple inheritance. Instead you need another dependency, NEXT.
So we are asking the person who implements AUTOLOAD to duplicate information (which they may not actually have), and then modify AUTOLOAD itself to follow best practices which not only are not widely documented, but which most developers cannot be expected to figure out on their own.
Now is it hopeless? Is there any way to have the goodness of something like AUTOLOAD and something like UNIVERSAL::can at the same time? I don't accept that, and I'm about to outline a design which makes the two play together.
Create a UNIVERSAL::AUTOLOAD that goes through the inheritance hierarchy looking for a CAN method. If it finds it, it calls that method expecting back a subroutine reference or a false value. If it gets a subroutine reference, it calls it. If it gets a false value, it continues looking. At the same time override UNIVERSAL::can to first try a regular UNIVERSAL::can (you can hold a reference to the original one in a closure), and otherwise do the same search through the inheritance tree for CAN methods that AUTOLOAD does. Convince everyone who wants to use AUTOLOAD that they should use your custom version and write CAN methods instead. And then it all works. You can accomplish anything that AUTOLOAD could. UNIVERSAL::can works. There is no duplication of logic anywhere. (If this explanation confuses and people really want, I could easily write this
Of course when it comes time to actually convince people to switch, you still have to get over their likely indifference to whether UNIVERSAL::can works. Particularly when it means rewriting already working code.
Perhaps in Perl 6 we can encourage people to follow this strategy. But in Perl 5 it is a lost cause, and it strikes me as fairly silly to lecture people on why they should do extra work to reduce breakage in something that they are highly unlikely to get right, and mostly didn't really care about anyways.
Update: I used the wrong word. Fixed.
Update 2: jweed pointed out that both links to past discussion were the same. Oops. Fixed.