Question: Can we use Perl's eval
construct in a "functional" way, in that die-or-no-die the construct evaluates to the same type of thing?
#!/usr/bin/env perl
use strict;
use warnings;
sub func1 {
my ($die_or_not) = @_;
my $res = eval {
print "inside eval\n";
if ($die_or_not) {
die "doing a die";
}
return {
status => 1,
message => "made it",
};
}
or do {
return {
status => 0,
message => "died: " . $@,
};
};
printf("STATUS: %d, MESSAGE: '%s'\n", $res->{status}, $res->{message});
return "yes";
}
printf("without die: %s\n", func1(0));
printf("with die: %s\n", func1(1));
Result:
inside eval
STATUS: 1, MESSAGE: 'made it'
without die: yes
inside eval
with die: HASH(0x55d998e254d8)
Conclusion: Not that way :-/ What happened? The return
inside the or do
block is treated as a return from the func1
sub, whereas the return
from the eval
block is assigned to $res
. This means our whole eval-or-do
doesn't evaluate to the same type of thing - in fact it's evaluation completely messes up program flow. Well, what about this weird way:
#!/usr/bin/env perl
use strict;
use warnings;
sub func1 {
my ($die_or_not) = @_;
return eval {
print "inside eval\n";
if ($die_or_not) {
die "doing a die";
}
return {
status => 1,
message => "made it",
};
}
or do {
return {
status => 0,
message => "died: " . $@,
};
};
}
my $wo = func1(0);
my $wi = func1(1);
printf("without die: %d:%s\n", $wo->{status}, $wo->{message});
printf("with die: %d:%s\n", $wi->{status}, $wi->{message});
Result:
Possible precedence issue with control flow operator at test.pl line 23.
inside eval
inside eval
without die: 1:made it
Use of uninitialized value in printf at test.pl line 29.
Use of uninitialized value in printf at test.pl line 29.
with die: 0:
Nope, not at all. It turns out that perl does not like a return
from an eval
, even though its happy to assign a variable from it. We can simulate what we want by writing a small wrapper called _evaly
:
sub _evaly {
my ($code, $err) = @_;
my $res = eval {
return $code->();
}
or do {
return $err->($@);
};
return $res;
}
This takes two code-refs. The first one is executed inside the eval, and if there is no die its return value is the result of the expression. If that code dies the $err
code-ref is executed and it's return value is the value of the expression. In this way the final result of the expression can be made uniform for the die and not-die case:
sub func1 {
my ($die_or_not) = @_;
return _evaly(sub {
if ($die_or_not) {
die "doing a die";
}
return {
status => 1,
message => "made it",
};
}, sub {
my ($err) = @_;
return {
status => 0,
message => "died: " . $@,
};
});
}
my $wo = func1(0);
my $wi = func1(1);
printf("without die: %d:%s\n", $wo->{status}, $wo->{message});
printf("with die: %d:%s\n", $wi->{status}, $wi->{message});
Result:
without die: 1:made it
with die: 0:died: doing a die at test.pl line 21.
Is there a better way?? What about Try::Tiny:
This is unlike TryCatch which provides a nice syntax and avoids adding another call stack layer, and supports calling return from the try block to return from the parent subroutine.
Sounds good, lets try:
#!/usr/bin/env perl
use strict;
use warnings;
use Try::Tiny;
sub func1 {
my ($die_or_not) = @_;
return try {
if ($die_or_not) {
die "doing a die";
}
return {
status => 1,
message => "made it",
};
}
catch {
return {
status => 0,
message => "died: " . $_,
};
};
}
my $wo = func1(0);
my $wi = func1(1);
printf("without die: %d:%s\n", $wo->{status}, $wo->{message});
printf("with die: %d:%s\n", $wi->{status}, $wi->{message});
Result:
without die: 1:made it
with die: 0:died: doing a die at test.pl line 11.
Horray! That looks nice! Now let's see if we can construct something that captures this unified thing inline:
sub func1 {
my ($die_or_not) = @_;
my $res = try {
if ($die_or_not) {
die "doing a die";
}
return {
status => 1,
message => "made it",
};
}
catch {
chomp;
return {
status => 0,
message => "died: " . $_,
};
};
$res->{extra} = "hello";
return $res;
}
In both cases this correctly adds the "extra" info the the return structure. The return
inside the try
and catch
blocks do not return from the func1
sub! So in summary, the pattern is like so:
use Try::Tiny;
my $result = try {
# Something that may or may not die
return <your type>;
}
catch {
# $_ has the error
return <your type>;
};
And now $result
will be <your type>
die-or-no-die! Can this be input to a function??
sub message {
my ($r) = @_;
printf("result: %d: %s\n", $r->{status}, $r->{message});
}
sub func1 {
my ($die_or_not) = @_;
message(try {
if ($die_or_not) {
die "doing a die";
}
return {
status => 1,
message => "made it",
};
}
catch {
chomp;
return {
status => 0,
message => "died: " . $_,
};
});
}
func1(0);
func1(1);
Result:
result: 1: made it
result: 0: died: doing a die at test.pl line 16.
Holy heck, that works perfectly! Why would you want this? Imagine you want to update the status of a record:
sub update {
my ($u, $r) = @_;
$u->set_status($r->{status});
$u->set_message($r->{message});
$u->save;
}
sub try_to_do_something {
my ($status_record, $something) = @_;
update($status_record, try {
$something->doit;
return {
status => 1,
message => "made it",
};
}
catch {
chomp;
return {
status => 0,
message => "died: " . $_,
};
});
}
See how try-catch
has become a composable thing with a return value, just like any other sub! With traditional eval that update flow would look like this:
sub try_to_do_something {
my ($status_record, $something) = @_;
my $status;
my $message;
eval {
$something->doit;
$status = 1;
$message = "made it";
1;
}
or do {
my $err = $@;
chomp $err;
$status = 0;
$message = "died: " . $err;
};
$u->set_status($status);
$u->set_message($message);
$u->save;
}
This sucks because:
The $status
and $message
variables are declared undefined before they are used. If you have an eval
block with many of lines of code, and an or do
with many lines of code, you can easily visually loose track of what state variables are in flight. With try-catch
you only have to look at return
expressions to understand what the result is. Ok, you could pre-declare them to a value, but then what values? You could declare $status
to 99 which would mean: "I forgot to manage state properly and accidentially broke the code".
Updating your status record is not tightly coupled with your try_to_do_something
sub. You could easily separate these things, and in normal flow just pipe the output of one func into the input of another. But for testing you can just call these subs separately.
You might have a very large function (as I did) doing all sorts of things with many state variables in flight. You can't safely refactor the entire function in one go to use eval
better, but you can easily introduce a try-catch
expression to better declare what the "output" of your doit
attempt should be!