Question

My question is similar to: Is it possible for a Perl subroutine to force its caller to return? but I need procedural method.

I want to program some message procedure with return, example essential code:

sub PrintMessage {
    #this function can print to the screen and both to logfile
    print "Script message: $_[0]\n";
}

sub ReturnMessage {
    PrintMessage($_[0]);
    return $_[2];  #  <-- we thinking about *this* return
}

sub WorkingProc {
    PrintMessage("Job is started now");
    #some code
    PrintMessage("processed 5 items");

    # this should return from WorkingProc with given exitcode
    ReturnMessage("too many items!",5) if $items>100;

    #another code
    ReturnMessage("time exceded!",6) if $timespent>3600;
    PrintMessage("All processed succesfully");
    return 0;
}

my $ExitCode=WorkingProc();
#finish something
exit $ExitCode

Idea is, how to use return inside ReturnMessage function to exit with specified code from WorkingProc function? Notice, ReturnMessage function is called from many places.

Was it helpful?

Solution

This isn't possible. However, you can explicitly return:

sub WorkingProc {
    PrintMessage("Job is started now");
    ...
    PrintMessage("processed 5 items");

    # this returns from WorkingProc with given exitcode
    return ReturnMessage("to much items!", 5) if $items > 100;

    ...
    return ReturnMessage("time exceded!", 6) if $timespent > 3600;
    PrintMessage("All processed succesfully");
    return 0;
}

A sub can have any number of return statements, so this isn't an issue.

Such a solution is preferable to hacking through the call stack, because the control flow is more obvious to the reader. What you were dreaming of was a kind of GOTO, which most people not writing C or BASIC etc. have given up 45 years ago.

Your code relies on exit codes to determine errors in subroutines. *Sigh*. Perl has an exception system which is fairly backwards, but still more advanced than that.

Throw a fatal error with die "Reason", or use Carp and croak "Reason". Catch errors with the Try::Tiny or TryCatch modules.

sub WorkingProc {
    PrintMessage("Job is started now");
    ...
    PrintMessage("processed 5 items");

    # this should return from WorkingProc with given exitcode
    die "Too much items!" if $items > 100;

    ...
    die "Time exceeded" if $timespent > 3600;
    PrintMessage("All processed succesfully");
    return 0;
}

WorkingProc();

If an error is thrown, this will exit with a non-zero status.

OTHER TIPS

The approach that springs to mind for non-local return is to throw an exception (die) from the innermost function.

You'll then need to have some wrapping code to handle it at the top level. You could devise a set of utility routines to automatically set that up.

Using Log::Any and Log::Any::Adapter in conjunction with Exception::Class allow you to put all the pieces together with minimum fuss and maximum flexibility:

#!/usr/bin/env perl

package My::Worker;
use strict; use warnings;

use Const::Fast;
use Log::Any qw($log);

use Exception::Class (
    JobException => { fields => [qw( exit_code )] },
        TooManyItemsException => {
            isa => 'JobException',
            description => 'The worker was given too many items to process',
        },
        TimeExceededException => {
            isa => 'JobException',
            description => 'The worker spent too much time processing items',
        },
);

sub work {
    my $jobid = shift;
    my $items = shift;

    const my $ITEM_LIMIT => 100;
    const my $TIME_LIMIT => 10;

    $log->infof('Job %s started', $jobid);

    shift @$items for 1 .. 5;
    $log->info('Processed 5 items');

    if (0.25 > rand) {
        # throw this one with 25% probability
        if (@$items > $ITEM_LIMIT) {
            TooManyItemsException->throw(
                error => sprintf(
                    '%d items remain. Limit is %d.',
                    scalar @$items, $ITEM_LIMIT,
                ),
                exit_code => 5,
            );
        }
    }

    { # simulate some work that might take more than 10 seconds
        local $| = 1;
        for (1 .. 40) {
            sleep 1 if 0.3 > rand;
            print '.';
        }
        print "\n";
    }
    my $time_spent = time - $^T;
    ($time_spent > $TIME_LIMIT) and
        TimeExceededException->throw(
            error => sprintf (
                'Spent %d seconds. Limit is %d.',
                $time_spent, $TIME_LIMIT,
            ),
            exit_code => 6);
    $log->info('All processed succesfully');
    return;
}

package main;

use strict; use warnings;
use Log::Any qw( $log );
use Log::Any::Adapter ('Stderr');

eval { My::Worker::work(exceptional_job => [1 .. 200]) };
if (my $x = JobException->caught) {
    $log->error($x->description);
    $log->error($x->error);
    exit $x->exit_code;
}

Sample output:

Job exceptional_job started
Processed 5 items
........................................
The worker spent too much time processing items
Spent 12 seconds. Limit is 10.

or

Job exceptional_job started
Processed 5 items
The worker was given too many items to process
195 items remain. Limit is 100.
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top