Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl-Sensitive Sunglasses
 
PerlMonks  

Handling cascading defaults

by markjugg (Curate)
on Aug 19, 2000 at 08:41 UTC ( #28627=perlquestion: print w/replies, xml ) Need Help??

markjugg has asked for the wisdom of the Perl Monks concerning the following question:

Hello, I'd like some advice on good perl style. I wrote a module for CGI error handling that I like the functionality of, but I think the interface needs some refinement.

The issue is this: The module needs to have some intelligent defaults, but these can be overridden by a module that calls it. For a given module that calls my error module (let's say Err.pm), it has it's own defaults, which can in turn be overridden a per-script basis.

Right now I have a routine in MyModule.pm which basically collects the values from the calling script, adds in defaults for abstent variables and returns Err::err.pm

It seems like there should be a slicker way than writing this wrapper routine to call the module though. Perhaps there is an answer somewhere in OO I can use?

I'll create some sample peusdo-code here in case someone wants to rework it for an example.

package Err.pm sub err { my %args = ( title =>'Really Bad Error', msg => undef, @_ ); # do something with %args here. } ########### package MyModule.pm; use Err.pm; sub err { my %args = ( title =>'My new default title for MyModule.pm', msg => 'MyModules generic error message', @_ ); return Err::err(%args); } ################## ### my script.cgi package MyModule; do_the_right_thing() || err(title=>'Oops',msg=>'something went wrong') +;

I hope that made sense. Thanks for any suggestions.

-mark

Replies are listed 'Best First'.
Re: Handling cascading defaults
by ncw (Friar) on Aug 19, 2000 at 11:38 UTC
    If you wanted to use OO then you could do this
    package Err; sub new { return bless { title => 'Really Bad Error', msg => 'Unknown', @_ }; } sub err { my %args = ( %{shift()}, @_ ); print "Error: $args{title}: $args{msg}\n"; } ########### package MyModule; $myerror = Err::new( title =>'My new default title for MyModule.pm', msg => 'MyModules generic error message' ); $myerror->err(); $myerror->err(title=>'Oops',msg=>'something went wrong');
    I was wondering whether you could do something using the __PACKAGE__ variable and a hash in the Err package, but I couldn't think of a way of reading your caller's package. I expect there is a way though. Then the err() function could read the callers package and read the default strings from a hash which the calling package had assigned.
      I would actually organize that slightly differently:
      package Err; sub new { return bless {}, shift; } sub err { my $self = shift; my %args = @_; # Do something with %args here }
      And then in other code you would do this:
      $handler ||= new Err; # time passes &do_right_thing() or $handler->err(title=>'Oops',msg=>'something went +wrong');
        At this point, since you've not shown the need for inheritance implementation reuse or even interface reuse, I'd stay away from the complexity of objects. My personal rule is not to pull in OO technology in Perl until the line count of the program exceeds about 1000 lines, and using all the OO features of abstraction, inheritance, and data hiding all becomes useful. So far, all you've done is named a data structure. {grin}

        -- Randal L. Schwartz, Perl hacker

      Thanks ncw. I had thought of an arrangement like the one above. I think the code design could be considered to cleaner, but here's what bothers me about it:

      One programming principle is "optimizing for the common case". The common case here is that I create a new "err" call in a script, as above. If I switch the OO style, instead of writing out 'err' each time, I'm now writing out '$err->err', so the solution makes more work in the common case.

      What about the magic that CGI.pm uses, where you can call routines with an OO style, or with a procedural style, and you don't even have to start with "new CGI", because there's a default object created. Can someone explain that?

      As far as fetching the caller's package, perl makes that easy: $callers_pkg = caller;

      I did end up using that in an inbetween module once, with the logic of "If my caller's package has defaults use those, otherwise use the defaults in CGI::Err". It worked, but that didn't seem clean either...

      Thanks for any further comment. -mark

        The trick that CGI uses is all in the following sub:
        sub self_or_default { return @_ if defined($_[0]) && (!ref($_[0])) &&($_[0] eq 'CGI'); unless (defined($_[0]) && (ref($_[0]) eq 'CGI' || UNIVERSAL::isa($_[0],'CGI')) # slightly optimized for common case ) { $Q = $CGI::DefaultClass->new unless defined($Q); unshift(@_,$Q); } return @_; }
        They preprocess the args to every function this way, and as long as the first arg is 'CGI' or an object that inherits from CGI they leave the arguments alone. Otherwise they prepend a global to the list.

        This is nice but imposes quite a bit of overhead.

        Another way of doing the same thing is to make the procedural interface be in a different package, and have a well thought-out AUTOLOAD sub that creates needed procedural wrappers at run-time. More complex but probably more efficient than what CGI does.

        Another solution that I have used is to think functionally. There you actually have your error handler be an anon sub. By default it is your default, but it can be anything you want. In this case you would leave your package alone and in your CGI script say:

        $handler ||= \&Err::err; # time passes do_the_right_thing() || $handler->(title=>'Oops',msg=>'something went +wrong');
        If $handler is left alone, well you get the default. But anyone who wants can change it.

        I have often found that this functional approach is a more efficient way to implement optional hooks. YMMV.

Re: Handling cascading defaults
by tye (Sage) on Aug 20, 2000 at 00:14 UTC

    Well, I disagree strongly that you should avoid OO just because the code is small. Just yesterday I wrote an OO module that was under 20 lines.

    At this point, since you've not shown the need for inheritance implementation reuse or even interface reuse, I'd stay away from the complexity of objects. My personal rule is not to pull in OO technology in Perl until the line count of the program exceeds about 1000 lines, and using all the OO features of abstraction, inheritance, and data hiding all becomes useful.

    Well, I find that implementation reuse and inheritance are way over emphasized in OO and almost never happen (outside of standard libraries that get shipped with the language). Although I see a small percentage of objects that do interface reuse (via inheritance), the majority don't do any of those things.

    In Perl the most common big advantages I see from OO are reduction of namespace collisions and simplified compartmentalization of configuration settings (my other favorite is cleaning up via destructors, but not all objects involve such). Here we want cascading defaults (ie. compartmentalization of configuration settings), which is done very nicely and naturely in Perl OO (I've never noticed it being done nicely in Perl in the absense of OO). I find that using Perl OO almost gives you cascading defaults without even trying.

    Have a package global that contains the default configuration parameters. Have a constructor that copies the configuration parameters from the package or object that was used to call it. Provide methods for changing configuration parameters (but only on constructed objects, not on the package global of sane defaults). (Heck, if writing methods seems like too much work, you can let the module user directly set the parameters using $obj->{param_name} -- though I think you'd be wrong both in thinking that writing methods is too much work and in encouraging such unverified manipulation of your object's members.)

    If you want script-level defaults, then you can also make this easy by making it easy to export/import a default error object for the script (see below).

    Also, a package that starts out at 20 lines could eventually grow to over 1000. Retrofitting OO at that point would be a huge pain. I wouldn't necessarilly start all projects as OO modules. But once you get to the point that you see a need to use the code from multiple scripts and that therefore it makes sense to turn it into a module, then I think it also usually (in Perl) makes sense to put in OO.

    package Err; use base qw( Exporter ); use Carp qw( croak ); my( %defaults, @options ); BEGIN { %defaults= ( Title => 'An error has occurred', Message => 'If error persists, seek assistance', ); @options= keys(%defaults); } sub new { my $this= shift; # Either a package name or an object. croak 'Usage: $handle= ',__PACKAGE__,"->new();\n", ' or: $handle= $ErrHandle->new()',"\n" if @ARGV; my $class= ref($this) || $this; # Copy defaults from calling object or use standard defaults: my $obj= ref($this) ? {%$this} : {%defaults}; return bless $obj, $class; } sub import { # Export a unique $ErrHandle to each caller: my $uniq= $_[0]->new(); *ErrHandle= \$uniq; goto &Exporter::import; } sub Configure { my( $self, $opts )= @_; croak 'Usage: $ErrHandle->Configure( ', '{ Title=>"new title", Message=>"new msg" } )', "\n" unless 2 == @ARGV && ref($opts) && UNIVERSAL::isa($opts,"HASH"); my @err= grep { ! exists $defaults{$_} } keys %$opts; croak __PACKAGE__,"::Configure: Unknown option(s) ", "(@err) not (@options)\n" if @err; @{$self}{keys %$opts}= values %$opts; return $self; }

    Now add a method that actually reports the error (you just had "do something" in your example). And you use it like this:

    use Err qw( $ErrHandle ); # Set script-specific defaults: $ErrHandle->Configure( {Message=>"Contact technical support"} ); # Use script defaults: $ErrHandle->Report(...); # Set and use other configurations: my $accountingErrHandle= $ErrHandle->new()->Configure( {Message=>"Please notify Bob"} ); $accountingErrHandle->Report(...);

    Note that you can now add a new configuration parameter by just adding one line specifying the name and default value for it. At 40 lines, it is more complex than what we started out with. But a lot of that has to do with the request for script-level defaults and some nice things I added like croak()ing when a method is called incorrectly.

    In summary, I think you can write very small but reasonable OO modules when you have something that starts out very simple but that you want to use several places. I think that in the long run you'll be glad you did start out with OO.

    Update: Fixed unmatched quotes in my still-thoroughly-untested sample code. Actually, I was forced to test the exporting of a unique scalar stuff before posting since I'd never actually done that before (actually).

            - tye (but my friends call me "Tye")
      (Posted based on private conversation with tye.)

      To me the key concept is abstraction layers. Perl offers a lot of ways to create an abstraction layer. As long as you have and think in terms of abstraction layers you will find cascading defaults trivial to implement.

      I have found this to be true even if the abstraction layer is something as simple as storing stuff in a hash. (Start it at the default, then overlay changes in hash slices.) The second pass at an OO answer in RE (tilly) 4: Handling cascading defaults shows this kind of abstraction allowing the over-riding of defaults set during your constructor in a method call. (OK, that was in an OO example, but the principle of how I cascaded that time was not OO specific.)

      Another form of abstraction is provided by using references to subs rather than subs directly. OO is yet another. All of these can solve cascading defaults quite smoothly, for the same basic reason - your code goes through a level of indirection which anything you want can be overridden in.

      Now what does OO provide above and beyond abstraction? Well it provides an entire mental framework for how to organize your thoughts about problems. For some people this fits well. For others it doesn't. I am a person who is both competent at thinking that way but doesn't particularly enjoy it or find it a natural fit. (YMMV of course!) I don't want to track down the link, but at IWETHEY we had a conversation about this once. I thought that OO reminded me very much of an area of math called category theory, both of which I disliked. Ironically someone else there knew both, agreed they were similar, and liked both of them. :-)

      I won't give a detailed critique of my feelings. Most of it wouldn't make sense to most people here because I compare it to my experiences with graduate level math, which is probably not a particularly common experience base in these parts. :-)

      However if you look at what I write you will find that for me abstraction layers are a theme that comes up quite often as a motivating principle. You also see it in some of my favorite links, such as Linus on Portability (linked on my home node).

Re: Handling cascading defaults
by mikfire (Deacon) on Aug 19, 2000 at 18:28 UTC
    I hope I have understood your question correctly. Your basic problem is that you want to have a default error handler that can be easily overridden, right?

    If so, then the trick we use in my shop works kinda like this:

    package Error; use var qw/$ERROR_HANDLER/; @ISA = qw/ EXPORTER /; @EXPORT_OK = qw/ &Error /; $ERROR_HANDLER = undef; # Because I need to be sure sub Error { if ( defined( $ERROR_HANDLER ) ) { $ERROR_HANDLER->($@); } else { print STDERR "@_\n"; } }
    The default behaviour is to print a nice message to STDERR. If somebody sets $ERROR::ERROR_HANDLER to a subroutine reference, though, that function will be called instead. Yes, there are a few holes in the code I have presented, but the idea works ( fixing them has been left as an excercise to the reader :).

    There isn't a lot of magic in CGI.pm. An object is simply a package, after all. To get the procedural methods made available, they simply put them in the @EXPORT. CGI.pm does use its own import method, but those are pretty easy to write as well. It is also pretty easy to write functions that can be used as either method calls or as procedural functions ( simply shift the first element off @_ and decide if it is an object reference of the correct type or not ).

    mikfire

Source and documentation for CGI::Err.pm
by markjugg (Curate) on Aug 19, 2000 at 21:22 UTC
    Thanks everyone for your responses. I've heard a range of possible solutions to address the issue, each with their own merits. I agree with merlyn that using full OO for this small module might be overkill. Some of the other solutions don't allow for two levels of defaults, one of the features I like about my current implementation.

    So what I've done is to document the code a bit as it is and release that publically. I'm still interested in further feedback on how to best make this work-- I'm hoping that by publishing the code and documentation the issue will become more clear.

    http://summersault.com/software/cascade/cgi_err/

    Any feedback is appreciated. Thanks!

    -mark

How bad is the original proposal?
by markjugg (Curate) on Aug 22, 2000 at 07:42 UTC
    Hello, You all have provided some great ideas on how to tackle cascading defaults. Some solutions have proposed OO possibilities, and others have used procedural methods, although I'm not sure these allow for two levels of defaults as I'm interested in. Now that I've heard some good ideas, I'd like to hear more about what's so bad about the original sort of structure proposed in the question. Here's my analysis of it:

    Merits

    • It's easy to understand. Because there is no OO abstraction layer, I think what the code is doing is more straightforward, and is therefore more accessible to folks to understand and extend. (Watch me contradict this below in the "Flaws" section)
    • Simple calling syntax. There's no need to initialize an object before calling err and I can type simply err rather than $obj->err, potentially saving a good bit of typing.
    Flaws
    • Awkward interface. I don't like using having the need for a wrapper subroutine in the users module, because it appears to give them a free form interface to manipulate stuff, where with OO you have a clearer interface of what options are valid and what aren't. It could also be construed as confusing to have CGI::Err:err and MyModule::err doing similiar but slightly different things if people didn't understand what was going on.

    Thanks! -mark

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://28627]
Approved by root
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others avoiding work at the Monastery: (5)
As of 2020-04-05 16:54 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    The most amusing oxymoron is:
















    Results (34 votes). Check out past polls.

    Notices?