Perl6-Control-Bail
Control::Bail:: Perl 6 module for deferred error cleanup
Purpose
The Control::Bail module allows nested allocations of resources to be
released in an orderly fashion, without repeating yourself, with no deep
block nesting and with deallocation code placed next to the corresponding
allocation code.
Remember this from your C days?
thing *allocthing() {
thing *res = malloc(sizeof(struct thing));
if (!thing) goto bail0;
thing.foo = malloc(sizeof(struct thing2));
if (!thing.foo) goto bail1;
thing.foo = malloc(sizeof(struct thing2));
if (!thing.foo.bar) goto bail2;
return thing;
bail2:
free(thing.foo);
bail1:
free(thing);
bail0:
fprintf(stderr, "Error allocating thing");
return NULL;
}
...While this had the advantage of avoiding deeply nested if/else
blocks, the code for freeing a resource was visually separated from
the code for allocating it. The other option was to make your
deallocators smart enough to realize when they were passed a partially
constructed object, so you could call it from the constructor
on error:
void freething(thing *) {
if (thing.foo.bar) free thing.foo.bar;
if (thing.foo) free thing.foo;
if (thing) free thing;
}
All in all that was not a horrible thing, and these days with GC we
often do not have to even bother with manual allocations -- but for
resources other than memory that need to be cleaned up quickly, like
TCP network connections, some languages have a construct, for example,
go's "defer" (example stolen from rosettacode:)
import "os"
func processFile() {
f, err := os.Open("file")
if err != nil {
// (probably do something with the error)
return // no need to close file, it didn't open
}
defer f.Close() // file is open. no matter what, close it on return
var lucky bool
// some processing
if (lucky) {
// f.Close() will get called here
return
}
// more processing
// f.Close() will get called here too
}
...which can help a lot, but you'd have to add tests to prevent the
deallocation from happening if you wanted to return the allocated object
from the function.
In Perl 6 there is a similar stack of "things that run after a function
returns" through the LEAVE/KEEP/UNDO phasers. These all exist on the same
queue (the LEAVE queue) but the KEEP and UNDO variety allow you to cancel
them by the way you return from the block. This is nice. However, unlike
defer, they are found and attached to the block at compile time:
sub makething () {
LEAVE { 42.say };
return "thing";
LEAVE { 43.say };
}
makething(); # Says 43 and then says 42.
In the above, even though the return "happens before" the second LEAVE block
visually, that LEAVE block still runs. So you still have to test whether
something has been done before trying to undo it.
This module solves that problem by providing a run-time version of these
phasers which works the way go's "defer" works -- it does not install the
cleanup code unless the flow of control actually reaches the statement.
The "bail", "trail", and "trail-keep" map to UNDO, LEAVE, and KEEP,
respectively.
Status
Brand spanking new. The names of the provided functions may end up
being capitalized or perhaps changed entirely. Let the bikeshedding
commence. Also, this module uses a lot of metamodel/internal stuff that
is not necessarily nailed down by specification, so it cannot promise
the same stability as one that uses only the tested 6.c features.
Idioms
# This DWYW. No need to test $skel or $thing to see whether or
# not they were allocated, works in reverse order of bail statements,
# and no bail statements get run when successful.
sub make_thing {
my $skel = make_skeleton();
$skel or die "Could not make skeleton";
bail { destroy_skeleton($skel); }
my $thing = make_skin($skel);
$thing or die "Could not make skin";
bail { destroy_skin($thing); }
Bool.pick or die("Unpredicable failure");
$thing;
}
# In the following code:
# If there was a touchdown there is cheering
# ...then...
# The Receiver gets an icepack, but only if he was tackled.
# ...then...
# The Receiver always gets juice, unless the QB was sacked.
# ...then...
# The QB always gets taunted, unless there was a touchdown.
# ...then...
# If there was no touchdown, the failure is thrown.
use Control::Bail;
sub towlboy {
bail { say "Taunt the QB" }
Bool.pick or die "sacked!";
trail { say "Bring Receiver juice" }
bail { say "Bring Receiver icepack" }
Bool.pick or die "tackled!";
say "touchdown!";
}
towlboy();