I received more feedback on Perl roles versus Duck Typing than any other entry in my series on Perl roles so far. Much of this feedback asked very good questions and pointed out places where I'd assumed that theory or implications were clear. Before I compare Perl roles to any other mechanism, it seems useful to clarify more about what I meant in my duck typing entry.
If you haven't also read The Why of Perl Roles, start there. Those design goals are important to understanding the benefits and drawbacks of other approaches.
Can't You Just Design Your Hierarchy Correctly?
That's true -- sometimes. If your data model fits nicely into a singly-rooted hierarchy, and if you can add variants at the leaves of the inheritance tree, inheritance works just fine.
Sometimes that's not possible. Ruby (and Perl and Python) make that process much easier. If your language supports multiple inheritance, you can have multiple parents (but as Perl Roles versus Inheritance describes, multiple inheritance has its own complications).
Can't You Code More Carefully?
I worry less about my code than I do about the code of other people. When I release code to the CPAN, I try to write robust code that's sufficiently generic (or polymorphic) that I don't prevent smart people from doing smart things I never anticipated. I respect my published interfaces (and theirs). I don't forbid subclassing or specialization. I respect encapsulation. Sometimes I even install tests that they can adapt and specialize if they adopt and specialize my code.
Of course I also try to write safe and robust code.
One of the mechanisms by which I try not to forbid other people from doing something I anticipated is to avoid forcing them to inherit from my classes. I could sprinkle
isa() checks throughout my code, but then they'd have to lie by overriding
isa() or inherit when they really wanted to delegate or reimplement or compose.
I could throw
can() checks to make sure that whatever they pass in supports a method of the appropriate name, but that leaves the code vulnerable to the false cognate problem.
I could ignore all of those possibilities and tell people that if the code breaks, they get to sweep up the pieces.
I prefer something safer and less prescriptive. If you pass my code an object which you've explicitly marked as performing a role we both understand, great! My code will do its best to work with it in a way that we both understand.
There may be bugs -- especially bugs of mutual misunderstanding of what that role implies -- but the role system will protect us against typos and incompleteness and collisions, and that's something you don't get from duck typing.
What if someone gets a role wrong?
Then your program has bugs, the same way as if someone gets inheritance wrong.
What if a role isn't a sufficient test of interoperability?
Then your program has bugs, the same way as if someone gets inheritance wrong.
What if two implementations of a role are incompatible in a given context?
I'm not sure what this means. If you compose two roles into a class and both roles supply a method of the same name, the role system will throw a compilation error. It won't try to disambiguate them; it cannot. You will have to do so.
This often means figuring out the context in which your class will perform one method or another and writing your own method which can dispatch appropriately.
Dog/Tree is a Dumb Example
Justin Donaldson wrote two longer, thoughtful responses: Duck Typing and "Roles" in Object Oriented Programming and Duck Typing and "Roles".
Justin's right; the "A dog can
bark() and a tree has
bark() example is silly." That's part of the reason it's a catchy
Perl cliché. It's a deliberately simple, deliberately dumb example you
can explain in a sentence or two to demonstrate a very real principle that
appears in code in much more subtle ways: similar words do not always mean
If you prefer an example from spoken languages, never tell a native Spanish speaker that you're embarazada when you mean that you're embarrassed.
Can't You Check Multiple Methods?
Justin pointed out that if checking for the existence of a single method on an invocant of unknown type is insufficient to determine its type equivalence, checking for multiple methods is safer. I agree -- but then you have to check for multiple methods (likely with your runtime reflection system), and you've only reduced your uncertainty of false cognates.
I'm sure you can see where this is going. Perhaps after you've checked three methods you've reduced your uncertainty sufficiently, at the cost of multiple lines of code and multiple runtime checks.
Perl Roles force you to specify roles explicitly
Yes, they do. To perform a role, add
does Name to your class declaration.
I joke. It's not quite that easy. For that to work, you must have identified a role in your system somehow, either by declaring it explicitly (similar to how you'd declare a class, except using the
role keyword instead of the
class keyword, for a savings of one character) or by declaring a class and treating that class as a role (an ability you get for free).
If your program is small and you're not worried about future extension or the cost of false cognates or duck typing problems, don't use roles. Don't use types. Don't use inheritance. That's perfectly acceptable. They're most valuable in larger systems where you do want pervasive polymorphism and extensibility without worrying about the drawbacks of ad hoc and unspecified invocations.
Checking methods is silly anyway; no one does that!
If duck typing's drawbacks weren't a problem, you wouldn't see
Wikipedia on Duck Typing recommending the use of Python
catch blocks for invoking methods on unknown invocants. If the method call cannot possibly fail, there's no reason to catch the "Hey, this method call failed! Hm!" exception.
can() to check that an invocant supports a
potential method is a well-worn idiom in Perl in the same way that checking
responds_to? is hoary Ruby.
I've never used Justin's haXe, so I'm completely unqualified to talk about how it solves these problems. I can only take his word for it. However....
There's no boilerplate if you predeclare a type!
Justin's followup argues that if you predeclare a type which defines an interface and use that interface pervasively throughout your system where you would normally use a type, you don't have to write boilerplate reflection code.
I agree, but I've categorized that as writing an interface (see Perl Roles versus Interfaces and ABCs): the act of explicitly declaring some combination of behavior and naming it and modifying your program to use that type instead of concrete, instantiable types.
You can do the same thing with roles. (You can also do more with roles.)
This is a fine technique, and I've used it to good effect. I don't classify it as duck typing, because it's an act of will to define this entity. I don't want to quibble over definitions, however.
Runtime reflection checks are rare in my code
Good! You don't run into the same problems I do.
Roles are rigid and top down
I'm not sure I agree with this. Most of the uses I've seen of roles are anything but rigid and top down. They're definitely more formal and structured than duck typing, but less so than subclassing inheritance. My colleague and sometimes co-author Ovid had a throwaway paragraph buried in his Flying Without Source Control rumination that using roles at the BBC has improved the flexibility of their code such that projects that would have taken multiple months now take minutes.
Roles violate Perl's loosely structured nature
"Violate" is a strong word, but a robust system is a robust system. Does Perl's test-infected culture violate its nature? Does the presence of an optional mechanism for managing code reuse and declaring the expected behaviors and relationships of entities without mandating their implementations of their behavior substantially change the nature of the language for the worse?
That's a philosophical question, and I'm not going to answer the worst case scenario. I prefer to believe that allowing the use of roles -- without mandating their use -- provides Perl with more opportunities to write great, robust, extensible, and understandable code. Certainly Ovid's writings demonstrate that his team has used them to great effect. (While he's one of the best and most reliable coders I've ever worked with, he'll be the first to disclaim any notions of rockstardom.)
I still don't get it
That's fine. Roles are subtle. I spent several years burning my fingers trying to combine code reuse with sufficient genericity and robust coding. When we saw Andrew Black present the Smalltalk Traits paper, a couple of my colleagues convinced me that what I was developing was sufficiently similar to the paper that Perl 6 could borrow their formalisms and achieve my goals.
If you really want to stretch your mind, consider this: what if your language had pervasive multi-dispatch built in, and dispatched not based on the class of all of its dispatchable invocants, but on the role of all of its dispatchable invocants? Remember, every class implies a role.
I don't intend to disparage any code that uses duck typing (or inheritance or interfaces or abstract base classes) successfully... but consider the implications in the previous paragraph. How would you even build such a thing using those techniques? Remember my design constraints for roles:
- They must not dictate the implementation of conforming entities, allowing inheritance or delegation or composition or reimplementation.
- They must not require editing existing entities to enable or improve polymorphic capabilities.
- They must be full-fledged members of the type system.
- They must provide compile-time disambiguation and refuse ambiguous composition.
If those aren't your design goals, that's fine. You can write a lot of useful, maintainable programs without them. Yet I believe we can write even more programs with them.