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

This is a followup to a question from 20 years ago.

I've got this test script for conditionally loading debug subroutines from a module I wrote:

#! /usr/bin/env perl use v5.14; use warnings; use if $ENV{MYMOD_DEBUG_LEVEL}, 'Test::Utils::Dump' => qw(d d0 d1 dd d +i $doff); d('hi'); di('hi'); dd('hi');

If the debug level is unset, the script will throw errors because the debug subroutines were not loaded. I could do this:

defined &d && d('hi'); defined &di && di('hi'); defined &dd && dd('hi');

But this is ugly. A less ridiculous solution is to write stubs for each sub I want to possibly load:

sub d {} sub d0 {} sub d1 {} sub dd {} sub di {}; use if $ENV{MYMOD_DEBUG_LEVEL}, 'Test::Utils::Dump' => qw(d d0 d1 dd d +i); d 'hi'; di 'hi'; dd 'hi';

But this still feels a big hacky. So I'm wondering if Perl Gods have bestowed upon us a gift for accomplishing this feat more cleanly and elegantly in recent years. Thanks.

EDIT: Using sub AUTOLOAD will catch subs that don't exist but from what I can tell, I would still need to wrap subroutine args in parens which I'd rather not do.

$PM = "Perl Monk's";
$MC = "Most Clueless Friar Abbot Bishop Pontiff Deacon Curate Priest Vicar Parson";
$nysus = $PM . ' ' . $MC;
Click here if you love Perl Monks

Replies are listed 'Best First'.
Re: Best practice for handling subroutines that are conditionally loaded?
by kcott (Archbishop) on Mar 08, 2024 at 16:13 UTC

    Not really part of what you're asking; however, I'd question the naming of some of the symbols you're exporting. Some general pointers:

    • Use more meaningful names.
    • dd() conflicts with Data::Dump's dd() — a possible problem now or down the track.
    • Exporting variable names is not recommended. See "Exporter: What Not to Export".
    • Consider using tags such that the export/import lists are controlled in one place; i.e. you don't need to make changes to every script when developing/maintaining, just to the module with the debug routines. See "Exporter: Specialised Import Lists".

    For your specific question, could you set up a Test::Utils::Dump::Noop whose debug routines have the same names as those in Test::Utils::Dump but are NO-OPs. Your test script(s) could then have something like:

    use if $ENV{MYMOD_DEBUG_LEVEL}, 'Test::Utils::Dump' => ':debug'; use if ! $ENV{MYMOD_DEBUG_LEVEL}, 'Test::Utils::Dump::Noop' => ':debug +';

    — Ken

      Thanks for the input. As far as meaningful names go, I just need something quick and dirty to type. And I type these so frequently, I'm not worried about forgetting them and I'm not sharing my debug module so I don't care. And I use a wrapper for Data::Dump in my debug module so I'm not worried about stomping on it. Besides, being able to break rules is what can make you a bad ass if you know what you are doing (or just an ass if you don't). :)

      On point three, I didn't know exporting vars was a bad idea. I'll keep that in mind.

      Point 4: yes, on my todo list.

      Regarding solution, I went with the solution in the comment above which is super clean, just one line. But it basically works like your suggestion. Thanks.

      $PM = "Perl Monk's";
      $MC = "Most Clueless Friar Abbot Bishop Pontiff Deacon Curate Priest Vicar Parson";
      $nysus = $PM . ' ' . $MC;
      Click here if you love Perl Monks

Re: Best practice for handling subroutines that are conditionally loaded?
by ikegami (Patriarch) on Mar 08, 2024 at 14:40 UTC

    Don't. Unconditionally export a constant a sub that does nothing.

Re: Best practice for handling subroutines that are conditionally loaded?
by etj (Priest) on Mar 09, 2024 at 08:21 UTC
    An idiom I learned from an ingy module, almost certainly Pegex, is:
    use constant DEBUG => $ENV{MY_DEBUG}; # ... DEBUG and print "something\n";
    The benefit is that constant-folding means that if it's not in debug mode, the Perl interpreter will simply optimise the debugging code out entirely.

      That's a beautifully elegant solution...
      ...and the interpreter optimisation is a big bonus :)

Re: Best practice for handling subroutines that are conditionally loaded?
by SankoR (Prior) on Mar 08, 2024 at 14:42 UTC
    It's early morning so I'm only coming up with awkward solutions but my brain says 'try UNIVERSAL::can.' You could do ... if __PACKAGE__->can('dd'), for example. This doesn't seem like a great idea though. I'd rather just call them and have them become no-op functions internally. You could turn them on and off at runtime that way as well.
Re: Best practice for handling subroutines that are conditionally loaded?
by LanX (Saint) on Mar 08, 2024 at 14:57 UTC
    There must be information missing...

    ...if the tests are all at one place, why not put them inside the same conditional if ($ENV{MYMOD_DEBUG_LEVEL}){...} ?

    Even better why don't you refactor the tests into a separate module which is used if ?

    You could even put the essential use Module there as long as you run it inside the same package

    Cheers Rolf
    (addicted to the Perl Programming Language :)
    see Wikisyntax for the Monastery

      My modules may have debug code sprinkled in:

      package Some::Useful::Module; sub my_mod_func { my ($self) = @_ dd $self; }

      So I want to conditionally load my debug code for this module.

      $PM = "Perl Monk's";
      $MC = "Most Clueless Friar Abbot Bishop Pontiff Deacon Curate Priest Vicar Parson";
      $nysus = $PM . ' ' . $MC;
      Click here if you love Perl Monks

        Either unsprinkle them or generate the empty function stubs automatically inside a BEGIN block.¹

        Having code all over the place calling "Schrödinger" functions doesn't strike me as a good design decision.

        The next maintainer might lock you in a box with randomly activated poison. ;)

        Update

        ¹) something like BEGIN { *{$_} = sub {} for @imports }

        NB: you can reuse @imports for your use if to keep it DRY

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        see Wikisyntax for the Monastery

Re: Best practice for handling subroutines that are conditionally loaded?
by Danny (Chaplain) on Mar 08, 2024 at 17:08 UTC
    For each routine in Test::Utils::Dump couldn't you just do something like:
    sub d0 { if($ENV{MYMOD_DEBUG_LEVEL} >= $my_level) { doThis; } else { doThat; } }
    In general, it might help to name your routines with a common tag so they are easy to find. For example my_debug_d0.
Re: Best practice for handling subroutines that are conditionally loaded?
by nysus (Parson) on Mar 08, 2024 at 15:55 UTC

    With a little help from my AI friend and after holding their hand a bit, I got this:

    package Test::Utils::Dump::Load; use v5.10; use strict; use warnings; use Exporter 'import'; our @EXPORT = qw(d d0 d1 dd di); my $debug = $ENV{MYMOD_DEBUG_LEVEL}; sub d { _load_and_call('d', @_) if $debug } sub d0 { _load_and_call('d0', @_) if $debug } sub d1 { _load_and_call('d1', @_) if $debug } sub dd { _load_and_call('dd', @_) if $debug } sub di { _load_and_call('di', @_) if $debug } sub _load_and_call { my $sub_name = shift; state $imported = 0; if (!$imported) { require Test::Utils::Dump; Test::Utils::Dump->import(qw(d d0 d1 dd di)); $imported = 1; } no strict 'refs'; ## no critic &{"Test::Utils::Dump::$sub_name"}(@_); } 1;

    So now in my module with debug code I can just add this one line:

    use Test::Utils::Dump::Load;

    $PM = "Perl Monk's";
    $MC = "Most Clueless Friar Abbot Bishop Pontiff Deacon Curate Priest Vicar Parson";
    $nysus = $PM . ' ' . $MC;
    Click here if you love Perl Monks

      That code only proves that AI isn't quite there yet. :)

      This is the 5 minute mess I just came up with:

      package Log::Maybe { use strict; use warnings; use parent 'Exporter'; our %EXPORT_TAGS = ( all => [ our @EXPORT = qw[info debug debugf $ +LOG_LEVEL] ] ); # Don't do this... our $LOG_LEVEL = $ENV{LOG_MAYBE}; + # 0: off, 1: debug, 2: info use Data::Dump qw[]; use Carp; $Carp::CarpLevel = 1; sub info { return 2 unless @_; return unless $LOG_LEVEL >= 2; Carp::cluck join ' ', map { ref $_ ? Data::Dump::dump($_) : $_ + } @_; } sub debug { return 1 unless @_; return unless $LOG_LEVEL >= 1; Carp::cluck '[' . localtime . '] ' . join ' ', map { ref $_ ? +Data::Dump::dump($_) : $_ } @_; } sub debugf { return 1 unless @_; return unless $LOG_LEVEL >= 1; Carp::cluck '[' . localtime . '] ' . sprintf shift, map { ref +$_ ? Data::Dump::dump($_) : $_ } @_; } }; 1;
      Which is used like so:
      use strict; use warnings; use lib './lib'; use Log::Maybe; # uses $ENV var $LOG_LEVEL = debug; # enable/change log leve +l info('oh, yeah, baby!'); # prints only if log lev +el is info debug('hi'); # prints if log level is + debug or info debug( 'wow', \%ENV ); # dumps non-scalars debugf( 'name: %s, age: %d', 'Jack', 23 ); # dumpf takes a sprintf +form $LOG_LEVEL = 0; # disable logging at run +time debug('nothing is logged'); # what it says on the ti +n

      I was feeling clever so the debug function gives you a stack trace. info and debug can also be used to return values used by $LOG_LEVEL. This is simple enough but something like Log::Any would be a wise choice here.

      Edit: Added debugf as an example of things you could do.

        > > With a little help from my AI friend

        (Well honestly 🤮)

        I recently saw a good metaphor for AI code.

        Someone used AI to create fake images of Trump surrounded by "happy" black voters for campaign reasons.¹

        Alas one pic had fantasy letters on the base caps, on the other pic Trump was missing fingers on one hand.

        Human perception might accept these, but computers won't correct fantasy code.

        Looking forward to the day the GOP nominates an AI for president, which nukes countries based on statistical grounds because of ... covfefe?

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        see Wikisyntax for the Monastery

        ¹) https://www.bbc.com/news/world-us-canada-68440150

      This looks overly convoluted to me.

      Just write an import (Don't necessarily need Exporter for that) routine which conditionally decides what to export at compile time.

      If you don't want to wire the ENV flag into the original module, just pass it's state as a flag while use -ing.

      I'm reluctant to provide code since your requirements are "drifting". (And you're friends with better qualified AI anyway)

      Cheers Rolf
      (addicted to the Perl Programming Language :)
      see Wikisyntax for the Monastery