jimad@microsoft.UUCP (Jim ADCOCK) (05/26/90)
In article <1990May23.171152.29448@Neon.Stanford.EDU> pallas@Neon.Stanford.EDU (Joe Pallas) writes: >Inheritance does NOT weaken encapsulation. You cannot "break" a class >by inheriting an implementation from it. You can, however, create a >broken class by inheriting an implementation from an unbroken class. >The correctness of an implementation says nothing about the >correctness of implementations which inherit from it. I'd claim in the following trivial example, the base class *is* broken -- but you cannot tell that by testing the base class without deriving from it. Conversely, the derived class *is not* broken -- but the bug in the base class only shows up when testing the derived class. So I claim that inheritence *does* weaken encapsulation, and you *can* break a class by inheriting an implementation from it -- if the base class is never derived from, then the present implementation of the base class is just fine! Only in the presence of derivation need the base class be fixed. class BASE { public: virtual void PrintClassName() { printf("BASECLASS\n"); } virtual void PrintBaseClassName() { PrintClassName(); } }; class DERIVED : public BASE { public: void PrintClassName() { printf("DERIVEDCLASS\n"); } }; main() { BASE* pbase; pbase = new BASE; pbase->PrintClassName(); pbase->PrintBaseClassName(); pbase = new DERIVED; pbase->PrintClassName(); pbase->PrintBaseClassName(); }
pallas@Neon.Stanford.EDU (Joe Pallas) (05/26/90)
In article <54873@microsoft.UUCP> jimad@microsoft.UUCP (Jim ADCOCK) writes: >I'd claim in the following trivial example, the base class *is* broken -- >but you cannot tell that by testing the base class without deriving >from it. Conversely, the derived class *is not* broken -- but the >bug in the base class only shows up when testing the derived class. >So I claim that inheritence *does* weaken encapsulation, and you *can* >break a class by inheriting an implementation from it -- if the base >class is never derived from, then the present implementation of the >base class is just fine! Only in the presence of derivation need the >base class be fixed. Does a class change somehow when you inherit from it? I think not. So how can the same class be both correct and incorrect? Only if we change our correctness criteria. If the specification of a class says nothing about inheritance (and, as I mentioned before, no one seems to have a good way of specifying such things yet), then what happens when you try to reuse the class cannot determine whether it meets its specification. If you make a decision to reuse code THAT MAKES NO PROMISE in its specification about its reusability, then YOU must take the responsibility for ascertaining its reusability---you are making a decision at least as significant as any other implementation decision, and the correctness of that decision is your responsibility. If some otherwise handy class is not reusable, you can go complain to whoever wrote it, but you cannot blame that person for your decision to reuse the code inappropriately. joe
cox@stpstn.UUCP (Brad Cox) (05/27/90)
In article <54783@microsoft.UUCP> jimad@microsoft.UUCP (Jim ADCOCK) writes: >In article <1990May20.154035.15064@axion.bt.co.uk> krichard@axion.bt.co.uk writes: >>Features of OOP such as encapsulation and well-defined interfaces >>no doubt facilitate the verification, validation and testing of >>object-oriented programs. On the other hand, others such as >>inheritance and dynamic binding would appear to make testing more >>difficult. >> In deciding whether object-oriented programs are more or less difficult to test, we must be careful to not confuse the chicken and the egg. The chicken is the object-oriented tools, where by 'object-oriented', I'm referring to open universe languages (Smalltalk, ObjectiveC) that do not insist that the relationship between parts and the whole be known and declared in advance, at compile-time, as the universe is being created by the compiler. In closed-universe languages (Ada, C++, Object-Pascal, etc), i.e. strongly type-checked languages, all such relationships must be known and declared at compile time. Open universe languages simply provide support for solving open-universe *problems* (collections of unknown components, pluggable software components, software components marketplaces, etc). But open universe problems are intrinsically more difficult to test than closed universe problems, independently of the tools used to build them. In other words, the testing difficulties arise from the nature of the problem, not from the nature of the object-oriented tools used to solve them. -- Brad Cox; cox@stepstone.com; CI$ 71230,647; 203 426 1875 The Stepstone Corporation; 75 Glen Road; Sandy Hook CT 06482
jimad@microsoft.UUCP (Jim ADCOCK) (05/30/90)
In article <5121@stpstn.UUCP| cox@stpstn.UUCP (Brad Cox) writes: |The chicken |is the object-oriented tools, where by 'object-oriented', I'm referring to |open universe languages (Smalltalk, ObjectiveC) that do not insist that the |relationship between parts and the whole be known and declared in advance, |at compile-time, as the universe is being created by the compiler. In |closed-universe languages (Ada, C++, Object-Pascal, etc), i.e. strongly |type-checked languages, all such relationships must be known and declared |at compile time. | |Open universe languages simply provide support for solving open-universe |*problems* (collections of unknown components, pluggable software components, |software components marketplaces, etc). | |But open universe problems are intrinsically more difficult to test than |closed universe problems, independently of the tools used to build them. | |In other words, the testing difficulties arise from the nature of the problem, |not from the nature of the object-oriented tools used to solve them. Well, clearly I disagree with both Brad's notion of "object-oriented" and "Open-Closed." Ideally, since in real-world libraries over half the classes are leaf classes, I'd like to be able to specify a leaf class as being such, and declare it "Closed." A class declared closed can no longer have its methods overridden, changing their meanings of its methods. This would allow a compiler to significantly optimize the closed class, and would allow testing the closed class as a module with fixed meaning. An object of a closed class could still be embedded [has-a], or inherited from for purposes of pure, orthogonal, extension -- you can add methods, but not change existing methods. Classes defined as being inheritable, and whose methods can be overridden would be considered "Open." I don't know how you really test these, but they still would be useful components for making closed modules. And at least closed modules can be reasonably tested.
rick@tetrauk.UUCP (Rick Jones) (05/30/90)
In article <54917@microsoft.UUCP> jimad@microsoft.UUCP (Jim ADCOCK) writes: >In article <5121@stpstn.UUCP| cox@stpstn.UUCP (Brad Cox) writes: >|The chicken >|is the object-oriented tools, where by 'object-oriented', I'm referring to >|open universe languages (Smalltalk, ObjectiveC) that do not insist that the >|relationship between parts and the whole be known and declared in advance, >|at compile-time, as the universe is being created by the compiler. In >|closed-universe languages (Ada, C++, Object-Pascal, etc), i.e. strongly >|type-checked languages, all such relationships must be known and declared >|at compile time. >| >|- etc - > >Well, clearly I disagree with both Brad's notion of "object-oriented" and >"Open-Closed." > >[discussion of open v. closed classes, etc] I agree with Jim that you cannot limit the world view of object-oriented programming to untyped languages of the Smalltalk school. Static typing with dynamic binding is much more appropriate in many (the majority?) of applications. The problem of safe inheritance has been nicely addressed in Eiffel in the form of assertions, the most important of which is the "class invariant". For those who don't know Eiffel, this is a list of boolean expressions in a class whose truth can be tested at run time every time a routine in the class returns to the caller. A failure of course generates a run-time exception. With inheritance, the invariants of all parent classes are also checked, i.e. invariants are inherited, and can only be added to, not replaced. This allows an inheriting class to overide a routine, but it must still conform to the invariant. There is a performance overhead in all this, so invariant checking is a compile-time option, and can be used to "prove" the software works, then compiled-out for a production version (assuming you're confident!). Even if you're not using Eiffel (biased opinion: you should be :-), the idea can be incorporated in other languages. Even C has a simple "assert" mechanism, has anyone tried extending this with C++? The hardest part is coding discipline, to ensure that you correctly express the semantics of the class within the assertions, but then coding discipline (or lack of it) is often the root cause of an awful lot of software errors! -- Rick Jones You gotta stand for something Tetra Ltd. Maidenhead, Berks Or you'll fall for anything rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick) - John Cougar Mellencamp
render@m.cs.uiuc.edu (Hal Render) (05/31/90)
In article <480@tetrauk.UUCP> rick@tetrauk.UUCP (Rick Jones) writes: >The problem of safe inheritance has been nicely addressed in Eiffel in the form >of assertions, the most important of which is the "class invariant". For those >who don't know Eiffel, this is a list of boolean expressions in a class whose >truth can be tested at run time every time a routine in the class returns to >the caller. A failure of course generates a run-time exception. > >With inheritance, the invariants of all parent classes are also checked, i.e. >invariants are inherited, and can only be added to, not replaced. This allows >an inheriting class to overide a routine, but it must still conform to the >invariant. Although class invariants are A Good Thing, I think that a more direct mechanism that prevents a method from being redefined in a subclass would be better in this case. There are already with different kinds of inheritance/visibility constraints for methods in some OOPLs, and this one sounds neither far-fetched nor difficult to implement. Further, it avoids the need for "coding discipline" that is the cause of many program errors. hal.
mckenney@sparkyfs.istc.sri.com (Paul Mckenney) (06/01/90)
In article <1990May30.204110.22011@ux1.cso.uiuc.edu> render@m.cs.uiuc.edu.UUCP (Hal Render) writes: >In article <480@tetrauk.UUCP> rick@tetrauk.UUCP (Rick Jones) writes: >>The problem of safe inheritance has been nicely addressed in Eiffel in the form >>of assertions, the most important of which is the "class invariant". For those >>who don't know Eiffel, this is a list of boolean expressions in a class whose >>truth can be tested at run time every time a routine in the class returns to >>the caller. A failure of course generates a run-time exception. >> >>With inheritance, the invariants of all parent classes are also checked, i.e. >>invariants are inherited, and can only be added to, not replaced. This allows >>an inheriting class to overide a routine, but it must still conform to the >>invariant. > >Although class invariants are A Good Thing, I think that a more direct >mechanism that prevents a method from being redefined in a subclass >would be better in this case. There are already with different kinds >of inheritance/visibility constraints for methods in some OOPLs, and >this one sounds neither far-fetched nor difficult to implement. >Further, it avoids the need for "coding discipline" that is the cause >of many program errors. > >hal. One major reason that testing inherited classes becomes complex is that classes often have member functions and variables that cooperate; these members must therefore be overridden in groups (or, if only a single member of the group is overridden, the programmer must very carefully ensure that the overriding function fits properly with the other members of the group). So, one approach would be to allow the programmer to specify groups of member functions/variable. The compiler could then give errors is some, but not all, members of a group were overridden. If the programmer ``knows what he is doing'', he can give the equivalent of a cast: class foo : public parent_of_foo . . . inline int bar() { parent_of_foo::bar(); } . . . }; by explicitly inheriting the parent's behavior. This approach of course requires additions to the languages in order to specify the member groups. However, it is a good fit to the major problem and adds absolutely no runtime overhead (at least for languages that allow inlining). Thanx, Paul
wdavis@x102c.harris-atd.com (davis william 26373) (06/01/90)
In article <1990May30.204110.22011@ux1.cso.uiuc.edu> render@m.cs.uiuc.edu.UUCP (Hal Render) writes: >In article <480@tetrauk.UUCP> rick@tetrauk.UUCP (Rick Jones) writes: >>The problem of safe inheritance has been nicely addressed in Eiffel in the form >>of assertions, the most important of which is the "class invariant". (rest of the Eiffel discussion deleted) >Although class invariants are A Good Thing, I think that a more direct >mechanism that prevents a method from being redefined in a subclass >would be better in this case. Why do you think it is better? To have a class be able to specify that a method cannot be redefined assumes that there is not a good reason for the redefinition. Even if there is no good reason at the time the class is written, there may be a good reason in the future. Just trying to determine if there could be a good reason at the time a class is designed can become an unbounded "what-if" game. The benefit of being able to extend a class via inheritance in a way that the original class implementor did not consider is a powerful method of software development. The "safe" aspect is the question of: "What did the original implementor intend as the constraints?" I think this is very well addressed in the notion of "Programming by contract" that is described by Dr. Meyer as the basis for the invariants used in Eiffel. Do you have some equally compelling reasons why the prevention should be considered better? > There are already with different kinds >of inheritance/visibility constraints for methods in some OOPLs, and Each constraint in a language comes because it has benefits that, in the opinion of the language designer(s), outweigh the cost that comes from not having the feature. Just because other languages have have certain restrictions, this does not mean those restrictions would be a good idea in a new language or even a new version of the same language. >this one sounds neither far-fetched nor difficult to implement. Just because it is easy to implement does not mean it is a good idea. >Further, it avoids the need for "coding discipline" that is the cause >of many program errors. Taking away valuable functionality because some people are going to have problems using it is not always the best way to design a language. Should we remove all loop constructs because it is possible to make a mistake and have the loop not terminate? We cannot have the language ensure that all loops terminate (unless someone has solved the general case of the "halting problem").
render@m.cs.uiuc.edu (Hal Render) (06/01/90)
In article <3754@trantor.harris-atd.com> wdavis@x102c.ess.harris.com writes: >In article <1990May30.204110.22011@ux1.cso.uiuc.edu> render@m.cs.uiuc.edu writes: >>Although class invariants are A Good Thing, I think that a more direct >>mechanism that prevents a method from being redefined in a subclass >>would be better in this case. > >Why do you think it is better? I think it is generally better to provide a direct solution to a problem rather than a round-about one. I have frequently had to rely on "coding discipline" to do something that I felt a language should offer me as a feature. In this case, the problem is someone redefining a method so that it no longer meets the original intent. My principal problem with relying on class invariants to prevent such redefinition is the difficulty one often has in formulating a useful invariant for a particular piece of code. I admit that part of my lack of confidence in this may be due to the lack of practice I have had in formulating invariants. >[Description of the value of redefinition ommitted.] >Do you have some equally compelling reasons why the prevention >should be considered better? Preventing errors is always a tricky thing. Upon reflection I do concede that it could cause just as many problems for the users to forbid them to redefine certain methods as it would be for the designer to formulate invariants that are strong enough to prevent undesirable redefinitions yet weak enough to allow desirable ones. The problems are subtle, and I will need to examine them more before I can say anything useful about the good/bad points of class invariants. >>this [feature] sounds neither far-fetched nor difficult to implement. > >Just because it is easy to implement does not mean it is a good idea. True, but my point was that difficulty of implementation would not be a point against it. >>Further, it avoids the need for "coding discipline" that is the cause >>of many program errors. > >Taking away valuable functionality because some people are going to >have problems using it is not always the best way to design a language. >Should we remove all loop constructs because it is possible to make >a mistake and have the loop not terminate? We cannot have the language >ensure that all loops terminate (unless someone has solved the general >case of the "halting problem"). I think you're reaching on this one. I did not propose outlawing redefinition, only the ability to prevent it for specific methods. I see the value in such language-supported constraints because I am now in the process of writing a fairly complex Smalltalk application. Frequently I find that I would like to be able to constrain the visibility of methods to both subclasses and container classes to prevent unintended violations of the semantics of the class. Unfortunately, Smalltalk does not support such constraints, so consequently I must rely on "coding discipline" to safeguard myself against errors. Since I am a fallible human, coding discipline has occasionally failed me, so I am leery of people who suggest it as an alternative for more direct language support. As I said, I do see the potential problems with being able to blindly restrict redefinition, but I have also faced the problems that such redefinition can cause in the conceptual integrity of a class hierarchy. Constraints, by their definition, are intended to prevent some sort of activity. We include them in a language to prevent "undesirable" activities. Of course, what seems undesirable for one person may seem highly desirable to another, so we have disputes such as this. hal.
pkr@media01.UUCP (Peter Kriens) (06/01/90)
Class invariants The discussion about class invariants and static type checking is a very crucial one. About this problem I have one question, do the static typing and class invariants not limit the future use of the code? Does the original programmer contain the wisdom to foresee in what way his code will be reused? I have found many times that I could reuse code which I was pretty sure the original programmer could never have thought of. This gave me reliable and debugged code. And doesn't this prevent more errors than relying on type checking and forbidding people to do something. Besidedes doesnt the dynamic binding ease the debugging process so much that there are hardly ever those obscure errors that I remember from pascal and other "save" languages? pkr pkriens@media01
dcr0@GTE.COM (David Robbins) (06/01/90)
From article <1990May31.224646.15066@ux1.cso.uiuc.edu>, by render@m.cs.uiuc.edu (Hal Render): > In article <3754@trantor.harris-atd.com> wdavis@x102c.ess.harris.com writes: >>In article <1990May30.204110.22011@ux1.cso.uiuc.edu> render@m.cs.uiuc.edu writes: >>>Although class invariants are A Good Thing, I think that a more direct >>>mechanism that prevents a method from being redefined in a subclass >>>would be better in this case. >> >>Why do you think it is better? > > I think it is generally better to provide a direct solution to a problem > rather than a round-about one. This last statement is really a *key* point. At least part of the problem under discussion here is that a subclass may redefine methods in a way that breaks non-redefined methods. In other words, the redefined method violates some assumption the broken method makes. While it is true that one way to avoid such problems is to prevent redefinition, that is not really a direct solution to the problem. The most direct solution to the problem is to provide a means by which *every* method could *completely* specify the assumptions it makes about other methods (and variables), such that the compiler (or some other tool) could verify that no assumption was violated. This solution (were it feasible) would permit the maximum possible flexibility in redefinition, without leaving the prevention of such errors entirely up to "coding discipline" (which, as we all know, is not quite infallible :-)). But of course we recognize that the current state of the art falls a wee bit short of being able to completely and formally specify all the assumptions a method makes about the world around it. So we must look around for a more practical solution that at least partially gives us what we need. Eiffel's assertions are a definite step in the right direction. The nature of these assertions (pre- and post-conditions, class invariants) is precisely that they say something about the assumptions a method (or a class) makes. Assertions, as provided by Eiffel, have some definite shortcomings. There are many significant assumptions that are difficult or impossible to code in the form of assertions, and others that can be coded, but are non-trivial to do and/or expensive to actually check. On the other hand, the very practice of thinking about assertions helps to provide and focus some "coding discipline" in a way that can significantly mitigate the problem under discussion. In my own (somewhat limited) experience with object-oriented programming, mostly in Smalltalk and Eiffel, I have encountered the problem of redefinition breaking things that work, and have experienced the benefits of using assertions. It is certainly true that Smalltalk gives you no help in this; your only recourse is to understand the parent class(es) well enough to know what you can and cannot do when redefining a method. If you spend enough time in Smalltalk, you may actually get fairly good at this. But I have also found that the use of assertions in Eiffel is a big win, in more ways than one. Relative to the problem at hand, the use of assertions makes it much easier for me to understand what I can and cannot do when redefining a method. Even an assertion that can only be stated in the form of a comment helps. I am always in favor of a language providing, somehow, a "direct solution" to programming problems. But what is a "direct solution"? If the problem is that a given method should absolutely never be redefined, then the language should make it possible to say so. If the problem is that a given method should only be redefined in a manner that is consistent with certain assumptions, then the language should make it possible to say so. I suspect that the problem is much more often the latter than the former; but I won't claim that there is *never* a good reason to absolutely forbid redefinition. The thing is, the former problem is easy to solve with today's language technology, while the latter is hard to solve. So, naturally, we may tend to perceive the problem as one we have an easy solution for. Someone once said that if the only tool you have is a hammer, every problem tends to look like a nail. We need to continue to search for the right tool for the right job. For the moment, we don't have all the right tools for prevention of the problems that can be introduced by redefining methods; but we do have some tools (e.g., assertions) that can help. -- Dave Robbins GTE Laboratories Incorporated drobbins@bunny.gte.com 40 Sylvan Rd. ...!harvard!bunny!drobbins Waltham, MA 02254 CYA: I speak only for myself; GTE may disagree with what I say.
plogan@mentor.com (Patrick Logan) (06/02/90)
In article <31960@sparkyfs.istc.sri.com> mckenney@sparkyfs.istc.sri.com (Paul Mckenney) writes: > One major reason that testing inherited classes becomes complex is that > classes often have member functions and variables that cooperate; these > members must therefore be overridden in groups (or, if only a single > member of the group is overridden, the programmer must very carefully > ensure that the overriding function fits properly with the other members > of the group). > > So, one approach would be to allow the programmer to specify groups of > member functions/variable. The compiler could then give errors is > some, but not all, members of a group were overridden. > > This approach of course requires additions to the languages in order to > specify the member groups. However, it is a good fit to the major problem > and adds absolutely no runtime overhead (at least for languages that allow > inlining). > Thanx, Paul At last Fall's Pacific Northwest Software Quality Conference, Fredrick Hart of the Georgia Institute of Technology presented an interesting paper titled "Constructing Reusable Software Using Feature Contexts". (Co-authored by John Shilling.) The abstract: "Explicit documentation of software features, using feature contexts, provides an effective way to create easily customized software components thus promoting reuse and enhancing maintainability." The paper describes "features" as "logical units of program functionality". And a "feature context" is "simply the realization of a feature in the environment". In this case the feature is "mutually dependent member functions and data". A feature context could be used to capture (realize, in their words) this feature for use in the process of specializing super class functionality in a subclass. Features and feature contexts are loosely related to database views and hypertext. I hope this description is somewhat clear. My point is that the functionality described by Paul does not have to be a programming language extension. It could be a part of the development environment and applied to more than one language and to languages that are not explicitly object oriented. Proceedings of the conference I mentioned can be ordered for $30 from: PNSQC P.O. Box 970 Portland, OR 97075 (It's the 1989 Pacific Northwest Software Quality Conference) It is full of useful and interesting papers. I can't make individual copies of this paper, sorry. Patrick -- Patrick Logan uunet!mntgfx!plogan | Mentor Graphics Corp. 8500 SW Creekside Pl | Beaverton, Oregon 97005-7191 |
cox@stpstn.UUCP (Brad Cox) (06/02/90)
In article <54917@microsoft.UUCP> jimad@microsoft.UUCP (Jim ADCOCK) writes: >In article <5121@stpstn.UUCP| cox@stpstn.UUCP (Brad Cox) writes: ||Open universe languages simply provide support for solving open-universe ||*problems* (collections of unknown components, pluggable software components, ||software components marketplaces, etc). | |Ideally, since in real-world libraries over half the classes are leaf classes, |I'd like to be able to specify a leaf class as being such, and declare it |"Closed." |Classes defined as being inheritable, and whose methods can be overridden |would be considered "Open." By "open universe languages", I was referring to encapsulation/binding, not to inheritance, which is a different matter altogether. -- Brad Cox; cox@stepstone.com; CI$ 71230,647; 203 426 1875 The Stepstone Corporation; 75 Glen Road; Sandy Hook CT 06482
rick@tetrauk.UUCP (Rick Jones) (06/04/90)
In article <9189@bunny.GTE.COM> dcr0@GTE.COM (David Robbins) writes: >From article <1990May31.224646.15066@ux1.cso.uiuc.edu>, by render@m.cs.uiuc.edu (Hal Render): >> In article <3754@trantor.harris-atd.com> wdavis@x102c.ess.harris.com writes: >>>In article <1990May30.204110.22011@ux1.cso.uiuc.edu> render@m.cs.uiuc.edu writes: >>>>Although class invariants are A Good Thing, I think that a more direct >>>>mechanism that prevents a method from being redefined in a subclass >>>>would be better in this case. >>> >>>Why do you think it is better? >> >> I think it is generally better to provide a direct solution to a problem >> rather than a round-about one. > >This last statement is really a *key* point. At least part of the problem >under discussion here is that a subclass may redefine methods in a way that >breaks non-redefined methods. In other words, the redefined method violates >some assumption the broken method makes. > > -- etc I think David has put his finger on the nerve of this thread. While most of the work in the field of OOPL's seems to be focussing on the mechanics of the languages, the wider issue of how they can support a reliable software design and engineering method is not really being addressed. As he says, Eiffel has made a significant step in the right direction with assertions, which while being far from perfect are a lot better than nothing. They are in fact one of the major reasons why I'm using the language. Eiffel's assertions support a semi-formal definition of the class interface in relation to clients. This discussion has been about the definition of the interface in relation to descendants, for which it (let alone any other language) does not provide any mechanism. I would very much like to see Eiffel take a new lead in addressing this problem. I believe that for software to be flexible and reliable, and for the great Nirvana of "bags of reusable software components" to become a serious reality, classes must *always* be extensible and adaptable by inheritance provided that they conform to whatever interface constraints are specified for the parent. Ducking the problem by saying "non-redefinable" is not the real answer; the constraints must be good enough to say what you mean. Traditionally issues like this have been regarded as part of the "development environment", rather than the province of the language itself, but one of the benefits of a good OO development strategy is that it can (and must) integrate everything. This ultimately means that OOPL designers have got to start incorporating serious constraint definition into the languages as well as all the standard stuff for expressing executable routines. IMO the supposed quantum leap in software productivity and reliability will not materialise and OO will all look like a lot of hype in a few years time unless the languages take on a wider view of what "programming" is, and embody capabilites which more represent "design & build". -- Rick Jones You gotta stand for something Tetra Ltd. Maidenhead, Berks Or you'll fall for anything rick@tetrauk.uucp (...!ukc!tetrauk.uucp!rick) - John Cougar Mellencamp