简体   繁体   中英

How can I conditionally define a Perl subroutine?

I want to define a Perl function (call it "difference") which depends on a command-line argument. The following code doesn't work:

if ("square" eq $ARGV[0]) {sub difference {return ($_[0] - $_[1]) ** 2}}
elsif ("constant" eq $ARGV[0]) {sub difference {return 1}}

It appears that the condition is ignored, and therefore the "difference" function gets the second definition regardless of the value of $ARGV[0].

I can make the code work by putting a condition into the function:

sub difference {
  if ("square" eq $ARGV[0]) {return ($_[0] - $_[1]) ** 2}
  elsif ("constant" eq $ARGV[0]) {return 1}
}

But this is not really my intention -- I don't need the condition to be evaluated each time during execution. I just need a way to influence the definition of the function.

My questions are:

  1. Why does the first construction not work?
  2. Why does it not give an error, or some other indication that something is wrong?
  3. Is there a way to conditionally define functions in Perl?

Others have already presented the syntax you requested, but I would recommend using more explicit subroutine references for this, so that you can freely manipulate the reference without manipulating the definition. For example:

sub square_difference { return ($_[0] - $_[1]) ** 2 }
sub constant_difference { return 1 }

my %lookup = (
    'square' => \&square_difference,
    'constant' => \&constant_difference,
);

my $difference = $lookup{$ARGV[0]} || die "USAGE: $0 square|constant\n";
print &$difference(4, 1), "\n";

It's the same basic approach, but I think this syntax will let you map arguments to subroutines a bit more conveniently as you add more of each. Note this is a variation on the Strategy Pattern , if you're into that kind of thing.

What you want to do can be achieved like this:

if ($ARGV[0] eq 'square') {
    *difference = sub { return ($_[0] - $_[1]) ** 2 };
}
elsif ($ARGV[0] eq 'constant') {
    *difference = sub { return 1 };
}

I haven't personally done a lot of this, but you might want to use a variable to hold the subroutine:

my $difference;
if ("square" eq $ARGV[0]) {$difference = sub {return ($_[0] - $_[1]) ** 2}}
elsif ("constant" eq $ARGV[0]) {$difference = sub {return 1}}

Call with:

&{ $difference }(args);

Or:

&$difference(args);

Or, as suggested by Leon Timmermans:

$difference->(args);

A bit of explanation - this declares a variable called $difference and, depending on your conditions, sets it to hold a reference to an anonymous subroutine. So you have to dereference $difference as a subroutine (hence the & in front) in order for it to call the subroutine.

EDIT: Code tested and works.

One more EDIT:

Jesus, I'm so used to use ing strict and warnings that I forget they're optional.

But seriously. Always use strict; and use warnings; . That will help catch things like this, and give you nice helpful error messages that explain what's wrong. I've never had to use a debugger in my life because of strict and warnings - that's how good the error-checking messages are. They'll catch all kinds of things like this, and even give you helpful messages as to why they're wrong.

So please, whenever you write something, no matter how small (unless it's obfuscated), always use strict; and use warnings; .

Subs are defined at compile time -> if you had "use warnings" enabled, you would have seen an error message about subroutine redefinition.

Other answers are correct, using either a code reference or an alias. But the aliasing examples introduce the yicky typeglob syntax and forget to deal with strict.

Alias is an oft forgotten module that wraps up all the magic needed to give a reference a name all while keeping strict.

use strict;
use Alias;

my $difference_method = $ARGV[0];
if( "square" eq $difference_method ) {
    alias difference => sub { return ($_[0] - $_[1]) ** 2 };
}
elsif( "constant" eq $difference_method ) {
    alias difference => sub { return 1 };
}
else {
    die "Unknown difference method $difference_method";
}

And now difference($a, $b) works.

If you only need to call difference() inside your own code, ie. you're not going to export it as a function, I would just use a code reference and forget the aliasing.

my $difference_method = $ARGV[0];

my $Difference;
if( "square" eq $difference_method ) {
    $Difference => sub { return ($_[0] - $_[1]) ** 2 };
}
elsif( "constant" eq $difference_method ) {
    $Difference => sub { return 1 };
}
else {
    die "Unknown difference method $difference_method";
}

$Difference->($a, $b);

Conditionally changing what a function does makes the code harder to follow and less flexible, just like changing behavior on any global. It becomes more obvious when you realize that you're just optimizing this:

my $Difference_Method = $ARGV[0];

sub difference {
    if( $Difference_Method eq 'square' ) {
        return ($_[0] - $_[1]) ** 2;
    }
    elsif( $Difference_Method eq 'constant' ) {
        return 1;
    }
    else {
        die "Unknown difference method $Difference_Method";
    }
}

Any time you have a subroutine of the form...

sub foo {
    if( $Global ) {
        ...do this...
    }
    else {
        ...do that...
    }
}

You have a problem.

Aliasing is most useful for generating similar functions at run time using closures, rather than cut & pasting them by hand. But that's for another question.

Thanks for all the suggestions for how to make the code work. Just for completeness, I'll give the high-level answers to my question.

  1. The first construction doesn't work because functions are defined at compile time but conditions and/or command-line arguments are evaluated at runtime. By the time the condition is evaluated, the named function has already been defined.

  2. The compiler does give a warning with "use warnings", though not one that's very useful for a programmer unaware of 1:-) The difficulty with giving a meaningful warning is that defining functions inside an if statement can make sense if you also do something with the function within the if statement, as in Leon Timmermans's suggestion. The original code compiles to a vacuous if statement, and the compiler is not set to warn about these.

  3. Strictly speaking, it is not possible to conditionally define functions, but it is possible to conditionally define references (rbright) or aliases (Leon Timmermans) to functions. The consensus seems to be that references are better than aliases, though I'm not quite sure why.

Note about 1: the evaluation order is not obvious until you've actually run into a problem like this; one could envision a Perl which would evaluate conditions at compile time whenever it could be done safely. Apparently Perl doesn't do this, since the following code too gives a warning about a redefined subroutine.

use warnings ;
if (1) {sub jack {}} else {sub jack {}}

Yet another way:

my $diffmode;
BEGIN { $diffmode = $ARGV[0] }
sub difference {
    if ($diffmode eq 'square') { ($_[0] - $_[1]) ** 2 }
    elsif ($diffmode eq 'constant')  { 1 }
    else { "It don't make no never mind" }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM