chocovore17
chocovore17

Reputation: 33

Call a subroutine defined as a variable

I am working on a program which uses different subroutines in separate files.

There are three parts

The subroutine takes its data from a text file.

I need the user to choose the text file, the program then extracts the name of the subroutine.

The text file contains

cycle.name=cycle01

Here is the main program :

# !/usr/bin/perl -w

use strict;
use warnings;

use cycle01;

my $nb_cycle = 10;

# The user chooses a text file

print STDERR "\nfilename: ";

chomp($filename = <STDIN>);

# Extract the name of the cycle 

open (my $fh, "<", "$filename.txt") or die "cannot open $filename";

while ( <$fh> ) {

    if ( /cycle\.name/ ) {

        (undef, $cycleToUse) = split /\s*=\s*/;
    }
}

# I then try to launch the subroutine by passing variables.
# This fails because the subroutine is defined as a variable.

$cycleToUse($filename, $nb_cycle);

And here is the subroutine in another file

# !/usr/bin/perl

package cycle01;

use strict;
use warnings;

sub cycle01 {

    # Get the total number of arguments passed
    my ($filename, $nb_cycle) = @_;
    print "$filename, $nb_cycle";

Upvotes: 1

Views: 1153

Answers (2)

Tanktalus
Tanktalus

Reputation: 22294

To build on Dave Cross' answer, I usually avoid the hash table, partly because, in perl, everything is a hash table anyway. Instead, I have all my entry-point subs start with a particular prefix, that prefix depends on what I'm doing, but here we'll just use ep_ for entry-point. And then I do something like this:

my $subname = 'ep_' . $cycleToUse;
if (my $func = Cycle01->can($subname))
{
  $func->($filename, $nb_cycle);
}
else
{
  die "$cycleToUse is not a valid subroutine name";
}

The can method in UNIVERSAL extracts the CODE reference for me from perl's hash tables, instead of me maintaining my own (and forgetting to update it). The prefix allows me to have other functions and methods in that same namespace that cannot be called by the user code directly, allowing me to still refactor code into common functions, etc.

If you want to have other namespaces as well, I would suggest having them all be in a single parent namespace, and potentially all prefixed the same way, and, ideally, don't allow :: or ' (single quote) in those names, so that you minimise the scope of what the user might call to only that which you're willing to test.

e.g.,

die "Invalid namespace $cycleNameSpaceToUse"
  if $cycleNameSpaceToUse =~ /::|'/;
my $ns = 'UserCallable::' . $cycleNameSpaceToUse;
my $subname = 'ep_' . $cycleToUse;
if (my $func = $ns->can($subname))
# ... as before

There are definitely advantages to doing it the other way, such as being explicit about what you want to expose. The advantage here is in not having to maintain a separate list. I'm always horrible at doing that.

Upvotes: 1

Dave Cross
Dave Cross

Reputation: 69314

Your code doesn't compile, because in the final call, you have mistyped the name of $nb_cycle. It's helpful if you post code that actually runs :-)

Traditionally, Perl module names start with a capital letter, so you might want to rename your package to Cycle01.

The quick and dirty way to do this is to use the string version of eval. But evaluating an arbitrary string containing code is dangerous, so I'm not going to show you that. The best way is to use a dispatch table - basically a hash where the keys are valid subroutine names and the values are references to the subroutines themselves. The best place to add this is in the Cycle01.pm file:

our %subs = (
  cycle01 => \&cycle01,
);

Then, the end of your program becomes:

if (exists $Cycle01::subs{$cycleToUse}) {
  $Cycle01::subs{$cycleToUse}->($filename, $nb_cycle);
} else {
  die "$cycleToUse is not a valid subroutine name";
}

(Note that you'll also need to chomp() the lines as you read them in your while loop.)

Upvotes: 4

Related Questions