Simplified error cleanup with Filter::Cleanup

Writing fault-tolerant programs can be a tedious exercise. Often, each step in the program logic depends on the success of the step prior, resulting in deeply nested calls to eval. Tracking the global $@ and ensuring that errors are not lost can be tricky and result in hard-to-follow logic.

I recently read The D Programming Language by Andrei Alexandrescu (which I highly recommend). D features an interesting (and, to the best of my knowledge, unique) alternative to try/catch-style error handling (although it also supports try/catch/finally). It turns out that very little code actually traps errors and makes decisions based on error conditions. In most cases, the error must be temporarily trapped to allow cleanup before re-throwing the error afterward. In these cases, D programmers can use a scope statement to register clean-up code to be executed should any of the statements following it trigger an error.

Imagine a function to mix two chemicals to trigger some reaction. You may have a container class that knows its own operating parameters as well as a recipe class that knows the chemical components and mixture rates.

void mix_dangerous_chemicals(Container c, Recipe r) {
    while (!r.is_complete()) {
        scope(exit) c.set_temp(r.get_temp(r.MIX_FAILURE));
        scope(exit) c.seal();

        // Add ingredients to the container
        c.add(r.ingredients, r.mix_rate);

        // If operating outside tolerances for the
        // container, trigger an error.
        c.validate_operating_parameters();

        // Check that the recipe is producing the
        // expected results, perhaps throwing an error
        // if the recipe sees that a reaction appears to
        // be ready to breach container tolerances.
        r.validate_procedure(c);
    }
}

This does something interesting. The scope statements each eat up all statements coming after them within their lexical scope and convert them into try/finally blocks. Statements are stack-based, so they are executed in the order opposite of declaration. This is because the first statement is wrapping the second scope statement in a try block, and then placing itself in the finally block. Something like this:

void mix_dangerous_chemicals(Container c, Recipe r) {
    while (!r.is_complete()) {
        try {
            try {
                // Add ingredients to the container
                c.add(r.ingredients, r.mix_rate);

                // If operating outside tolerances for the
                // container, trigger an error.
                c.validate_operating_parameters();

                // Check that the recipe is producing the
                // expected results, perhaps throwing an error
                // if the recipe sees that a reaction appears to
                // be ready to breach container tolerances.
                r.validate_procedure(c);
            } finally {
                c.seal();
            }
        } finally {
            c.set_temp(r.get_temp(r.MIX_FAILURE));
        }
    }
}

Which is cool, because the first syntax is much easier to read. You can imagine what this does for really hairy error handling.

Filter::Cleanup

Filter::Cleanup makes this syntax possible in Perl using source filtering and PPI. PPI is used to ensure that the rest of a cleanup statement's scope is addressed and correctly rewritten (because let's face, correctly parsing Perl with regexes is hard). Using the cleanup source filter, the above code would be written like this:

use Filter::Cleanup;

sub mix_dangerous_chemicals {
    my ($c, $r) = @_;

    until ($r.is_complete()) {
        cleanup { $c.set_temp($r.get_temp($r.MIX_FAILURE)) };
        cleanup { $c.seal() };

        $c.add($r.ingredients, $r.mix_rate);
        $c.validate_operating_parameters();
        $r.validate_procedure();
    }
}

Filter::Cleanup is available on CPAN and github.

2 Comments

See Scope::Cleanup for a solution that doesn't source filters at all.

Leave a comment

About Jeff Ober

user-pic I blog about Perl.