2010-11-14

Handling optional requirements with Class::Load

In a previous blog I discovered Class::Load and its awesomeness.

Here is one practical application of it:

--> UPDATES

--> UPDATES2

Automatic Optional Requisites

Say you have a library which provides some form of extensibility to consuming modules, and you want a way to "magically" discover a class to use, but use a fallback if its not there.

Here is how to do it with Class::Load

use strict;
use warnings;
package Some::Module;
use Class::Load qw( :all );

sub do_setup { 
    ...
}
sub import {
    my $caller = caller(); 
    my $maybemodule = "$caller::Controller";
    if( try_load_class( $maybemodule ) ){ 
        do_setup( $maybemodule ); # its there, and it works.
    } else { 
        if ( $Class::Load::ERROR =~ qr/Can't locate \Q$maybemodule\E in \@INC/ ){ 
           do_setup("Some::Module::Default");
        } else { 
           die $Class::Load::ERROR;
        } 
    }
}
To do it the right way without Class::Load is extraordinarily complicated.
use strict;
use warnings;
package Some::Module;

sub do_setup { 
    ...
}
sub import {
    my $caller = caller(); 
    my $maybemodule = "$caller::Controller";

    # see rt.perl.org #19213
    my @parts = split '::', $class;
    my $file = $^O eq 'MSWin32'
             ? join '/', @parts
             : File::Spec->catfile(@parts);
    $file .= '.pm';
      
    my $error;
    my $success;
    { 
       local $@;     
       $success = eval { 
          local $SIG{__DIE__} = 'DEFAULT';
          require $file;
          'success';
       };
       $error = $@;
    }
    
     if( $success eq 'success' ) ){ 
        do_setup( $maybemodule ); # its there, and it works.
    } else { 
        if ( $error =~ qr/Can't locate \Q$maybemodule\E in \@INC/ ){ 
           do_setup("Some::Module::Default");
        } else { 
           die $error;
        } 
    }
}

And even then, you still have a handful of sneaky bugs lurking in there :/

  1. With the second code, if somebody dynamically created the ::Controller class and didn't create a file for it, it will not work properly, and they'll have to tweak $INC somewhere for it to work
  2. If somebody loaded the ::Controller class manually before hand, but it failed, and they didn't report the error, on 5.8, the above code will behave as if the code Did load successfully. ( Truely nasty )

Class::Load has a lot of heuristics in it to try avoid both these situations ( well, the latter one will be soon once a 1-line patch goes in )

There are a few things still that I don't like doing that way, but for now, that's the best I can get

  1. Using a regular expression to determine what type of load failure occurred is nasty, but the only alternative approaches are either
    1. more complicated
    2. prone to be wrong on 5.8

What I'd like to be able to do

and may write a patch for

use strict;
use warnings;
package Some::Module;
use Class::Load qw( :all );

sub do_setup { 
    ...
}
sub import {
    my $caller = caller(); 
    my $maybemodule = "$caller::Controller";
    if( try_load_working_class( $maybemodule ) ){ 
        do_setup( $maybemodule ); # its there, and it works.
    } else {
        do_setup("Some::Module::Default"); #its not there.
    }
}

The idea being "Syntax errors are syntax errors, there's no good reason to suppress them , at all", so in the above code, if Whatever::Controller existed, but was broken, it would die, instead of treating it as if it were absent.

UPDATE

Module Patched and on github! =). Waiting on an authoritative update =)

package App;
use Class::Load qw( :all );

sub import { 
   my $caller = caller();
   my $baseclass = load_optional_class("${caller}::Controller") ? "${caller}::Controller" : "App::Controller";
   push @{$caller}::ISA, $base_class; # this line is pseudocode.
}

UPDATE #2

On CPAN: http://search.cpan.org/~sartak/Class-Load-0.06/lib/Class/Load.pm.
Thanks Sartak =)

3 comments:

  1. Thanks a lot! This is exactly what I need for WebNano :)

    ReplyDelete
  2. Hey Kent,

    Thanks for your patches and blog post. :) I've just released 0.06 with your changes and some additional changes by Jesse Luehrs.

    Shawn

    ReplyDelete
  3. Thanks Shawn =). Now to go rewrite a few things to use that ;)

    ReplyDelete