Is It, Can It, Does It, and Robust Perl 5 OO

A recent thread on p5p about warnings from Pod::Abstract with Perl 5.11.3 brought up an old debate over the nature of the UNIVERSAL package in Perl 5.

A recent deprecation of importing isa and can as functions from UNIVERSAL is the source of the warnings. This has the potential to affect plenty of code, because plenty of existing Perl 5 code does the wrong thing in the wrong way.

That's not entirely the fault of the coders; there's almost no right way to ask the right question in the right way. This is a design flaw in Perl 5.

The problem is that you can't always be sure that a scalar you have supports the operations you want to perform on it. For example, suppose that you receive as a parameter an object that may or may not allow logging with the log() method. If you call log() on it, you may get an exception.

One approach is to call can() on the object to see if Perl 5 knows about a log() method defined on the object:

if ($obj->can( 'log' ))
{
    ...
}

That's all well and good (except that it is susceptible to the false cognate problem) until $obj may not in fact be a valid object at all. In that case, trying to call a method on a non-invocant may not work.

I don't know why you'd write an API where this is possible, but apparently it's a popular hobby.

Some people think that the proper approach is to avoid the method call altogether. I used to think this too. I was wrong. Here's the broken code they write:

if (UNIVERSAL::can( $obj, 'log' )) # broken code; do not use
{
    ...
}

The warning introduced in 5.11.3 by the linked patch doesn't catch this case. (You have to use the shouldn't-be-so-controversial UNIVERSAL::can module to get warnings in this case.) It only catches the case where someone has written:

use UNIVERSAL 'can'; # broken code; do not use

if (can( $obj, 'log' )) # broken code; do not use
{
    ...
}

The safest approach is to change the API so that you never have to guess if you have a valid invocant. If you can't do that, the safest check is:

if (eval { $obj->can( 'log' ) })
{
    ...
}

... to catch the exception (or use Try::Tiny).

This is a pattern repeatable with isa as well.

As mentioned earlier, this still leaves the code open to false cognate problems, where the mere presence of a method (or function) named log() is not sufficient to determine whether that method (or function) actually behaves as expected.

This is the purpose of the DOES() method added in Perl 5.10 — if you use roles instead of duck typing.

Why is it unsafe to call can() as a function? AUTOLOAD(). The implementation of can() in UNIVERSAL has no idea if any class or object will generate its own methods. It only knows how to look in the appropriate namespace for CVs; it can't even distinguish between functions and methods. Of course, if you write your own AUTOLOAD for a class, you must also override the can() method to return the appropriate sub reference when queried. Code that neglects to do so — far too much code, in truth — is also broken. (Then you can have the fun experience of debugging other modules which call can() as a function and break your carefully-written objects.)

Things get trickier when dealing with isa(); more about that next time.

Modern Perl: The Book

cover image for Modern Perl: the book

The best Perl Programmers read Modern Perl: The Book.

sponsored by the How to Make a Smoothie guide

Categories

Pages

About this Entry

This page contains a single entry by chromatic published on December 30, 2009 10:50 AM.

More Perl Packaging Possibilities was the previous entry in this blog.

UNIVERSAL and API Decisions is the next entry in this blog.

Find recent content on the main index or look in the archives to find all content.


Powered by the Perl programming language

what is programming?