NAME

Worlogog::Incident - Lisp-style resumable exceptions (conditions)

SYNOPSIS

use Worlogog::Incident -all => { -prefix => 'incident_' };
use Worlogog::Restart  -all => { -prefix => 'restart_' };

sub log_analyzer {
  incident_handler_bind {
    for my $log (find_all_logs()) {
      analyze_log($log);
    }
  } [
    'MalformedLogEntryError' => sub {
      restart_invoke 'skip_log_entry';  # ignore invalid log entries
      
      # we could also do this:
      #restart_invoke 'use_value' => MalformedLogEntry->new(text => $_[0]);  # use invalid log entries as-is
    },
  ];
}

sub analyze_log {
  my ($log) = @_;
  for my $entry (parse_log_file($log)) {
    analyze_entry($entry);
  }
}

sub parse_log_file {
  my ($file) = @_;
  open my $fh, '<', $file or die "$0: $file: $!\n";
  my @results;
  while (my $text = readline $fh) {
    chomp $text;
    my $entry = restart_case {
      parse_log_entry($text)
    } {
      skip_log_entry => sub { undef },
    };
    push @results, $entry if $entry;
  }
  @results
}

sub parse_log_entry {
  my ($text) = @_;
  if (is_well_formed_log_entry($text)) {
    return LogEntry->new(... $text ...);  # parsing details omitted
  }
  restart_case {
    incident_error MalformedLogEntryError->new(text => $text);
  } {
    use_value     => sub { $_[0] },
    reparse_entry => sub { parse_log_entry($_[0]) },
  }
}

DESCRIPTION

This module provides resumable exceptions ("conditions") similar to those found in Common Lisp. A condition is a bit like an exception where the exception handler can decide to do other things than unwind the call stack and transfer control to itself.

A note on naming: This module doesn't follow Common Lisp terminology (there are conditions and you can signal them) because I think it would be confusing: The controlling expressions in if or while are also called conditions, and "signal" is more closely associated with %SIG and kill.

Instead I will refer to incidents, which you can report.

Functions

The following functions are available:

handler_bind BLOCK CODEREF
handler_bind BLOCK ARRAYREF

Executes BLOCK while registering the incident handler(s) speficied by CODEREF or ARRAYREF (see below). If (during the execution of BLOCK) an incident is reported, the innermost registered handler is called with one argument (the incident object).

An incident handler can either return normally, which causes the incident machinery to call the next active handler on the stack, or abnormally, which terminates the incident report. Abnormal returns are possible directly with Return::MultiLevel or indirectly via Worlogog::Restart. That is, an incident handler can abort an incident in progress by invoking a restart that transfers control to an outer point in the running program.

If an ARRAYREF is used, it is passed on to dispatch in Dispatch::Class (which see for details) to assemble the incident handler. The format of the ARRAYREF is that of a dispatch table, i.e. a list of CLASS, CODEREF pairs where CLASS specifies what class of exception objects should be handled (use the pseudo-class :str to match plain strings), and CODEREF is the actual body of the handler.

handler_case BLOCK CODEREF
handler_case BLOCK ARRAYREF

Similar to handler_bind above. The main difference is that an incident handler established by handler_case has to decide up front whether to handle the incident, and if it does, the stack is unwound first.

The way this is done depends on the second argument: If it's an ARRAYREF, the class of the incident object decides whether a handler (and which one) is entered.

If it's a CODEREF, it is called with the incident object as its first (and only) argument. If the CODEREF returns another coderef, that is taken to be the handler (i.e. the stack is unwound, the returned (inner) coderef is called with the incident object, and the return value of the inner coderef is returned from handler_case). If the CODEREF returns undef, the search for an incident handler that feels responsible continues outwards.

report INCIDENT

Reports an incident; i.e. it searches the call stack for all appropriate incident handlers registered with handler_bind or handler_case. It calls them in order from innermost (closest to the report call) to outermost (closest to the main program), then returns.

An incident handler can decline to handle a particular incident by returning normally. Conversely, an incident handler can stop a report in progress by not returning normally. This can be done by throwing an exception (die), using a non-local return (Return::MultiLevel), or invoking a restart that does one of these things (Worlogog::Restart).

Incident handlers registered with handler_case implicitly perform a non-local return.

error INCIDENT

Reports INCIDENT. If no handler takes over and handles the incident, throws INCIDENT as a normal Perl exception. Equivalent to:

sub error {
  my ($incident) = @_;
  report $incident;
  die $incident;
}
cerror INCIDENT

Works like error with a restart called 'continue' wrapped around it:

sub cerror {
  my ($incident) = @_;
  restart_case {
    error $incident;
  } {
    continue => sub {},
  };
}

This way an incident handler can invoke 'continue' to prevent the exception that would normally occur from being thrown.

warn INCIDENT

Reports INCIDENT. If no handler takes over and handles the incident, outputs INCIDENT as a warning via Carp::carp unless the restart 'muffle_warning' is invoked. Equivalent to:

sub warn {
  my ($incident) = @_;
  restart_case {
    report $incident;
    carp $incident;
  } {
    muffle_warning => sub {},
  };
}

This module uses Exporter::Tiny, so you can rename the imported functions at use time.

SEE ALSO

Exporter::Tiny, Worlogog::Restart, Return::MultiLevel, Dispatch::Class

AUTHOR

Lukas Mai, <l.mai at web.de>

COPYRIGHT & LICENSE

Copyright 2013, 2014 Lukas Mai.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.