0

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.

Community
  • 1
  • 1
Znik
  • 1,047
  • 12
  • 17

3 Answers3

3

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.

amon
  • 57,091
  • 2
  • 89
  • 149
  • Thanks for answer, first method I'm using now, with exitcode routing (using ReturnMessage as return parameter). I'll try Try:Tiny / TryCatch as you suggested. I must care because WorkingProc is called many times. It does not matter previos call is clean or not. Main procedure decides it is sense or not next loop, it is depended on exit code. – Znik Aug 19 '13 at 13:27
  • I found another exception catcher: Try::Tiny::SmartCatch . TryCatch is very poor, Try::Tiny very interesting. I'll use this. But i found very fresh Try::Tiny::SmartCatch, under this stack of 'catch' filter intercepts named exception generated by 'throw' in 'try' block. very nice, similar to java :) you can found this here: https://github.com/mysz/try-tiny-smartcatch – Znik Aug 20 '13 at 13:02
  • @znik The `Try::Tiny::SmartCatch` can be installed [from CPAN](https://metacpan.org/module/Try::Tiny::SmartCatch). This is preferable to copying code from github. However, I urge you to use vanilla `Try::Tiny`: This is the de-facto standard, and solves basically all problems that the traditional error handling with `eval { ... }; if ($@) { ... }` has. Whatever you end up using, I think it is good that you want to use proper exception handling. – amon Aug 20 '13 at 20:17
  • of course. I decided to use Try::Tiny and made some sucessfully experiments. But SmartCatch is really interesting. I noticed it's fresh, and meant not recommended to production using. I pointed page outside CPAN because I was unable to get help about SmartCatch package on the CPAN. Thanks for suggestions :) – Znik Aug 21 '13 at 07:15
2

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.

drquicksilver
  • 1,627
  • 9
  • 12
2

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.
Sinan Ünür
  • 116,958
  • 15
  • 196
  • 339
  • Very interesting approach. I think it is adaptable to my problem but I'll use simple exception handling Try::Tiny . Some problem to resolve is, nemaking next step is depended on previos step. When fatal error is returned, doing next step is nonsense (disk full for example). When 'averrage' error is returned, next step is usually possible. I belive your approach is possible to adapt to that algorithm. – Znik Aug 21 '13 at 07:18