miércoles, 4 de noviembre de 2009

Manejando errores en Perl

[English Translation]

En Perl el manejo de excepciones es un poco diferente al que probablemente estamos acostumbrados, en particular Perl no tiene try/catch/throw como algunos otros lenguajes, pero no quiere decir que no hay manejo de excepciones, Perl puede capturar y manejar las excepciones tan bien como cualquier otro lenguaje solo que las estructuras son ligeramente diferentes.

El manejo de excepciones en Perl se basa en el uso del operador eval, que permite evaluar código y capturar errores, cuando eval recibe una cadena de caracteres, compila el código que esta en la cadena y lo ejecuta, sin embargo cualquier error que suceda en este código, desde la compilación hasta la ejecución abortará únicamente el eval y nuestro programa seguirá funcionando, por ejemplo:

1 use Modern::Perl;
2 my $result = eval( "5 / 0" );
3 say "El resultado es: $result";

Aunque el programa funciona, el resultado del eval es undef, porque la división por cero evitó que se retorne algún valor, esto además causa una advertencia en la línea 3 sobre la concatenación con una variable indefinida.

Lo que necesitamos es saber si el eval fué exitoso o no, y eso esta en la variable especial $@ (tambien conocida como $EVAL_ERROR si usamos el módulo English).

Así que para capturar la excepción solo verificamos $@ después del eval:

1 use Modern::Perl;
2 my $result = eval( "5 / 0" );
3 if ( $@ ) {
4     say "Ooops: $@";
5 }
6 else {
7     say "El resultado es: $result";
8 }

Lo que captura el error, el problema con esta solución es que el código que está dentro del string no se verifica a tiempo de compilación, sino que se compila al momento de ejecutarse, y aunque esto es sumamente poderoso, en la mayoría de los casos lo que nos interesa de eval es la capacidad de capturar errores, para ello la segunda forma de eval, recibe un bloque de código que se verifica durante la compilación del programa, y la podemos usar así:

2 my $result = eval { 5 / 0 };

En esta forma de eval las llaves ({}) marcan el bloque donde se requiere capturar excepciones y retorna la última expresión del bloque, o undef si sucede algún error de ejecución (porque los errores de sintaxis ya fueron capturados durante la compilación del programa).

La ultima primitiva que necesitamos para completar el sistema de excepciones de Perl es die, que permite lanzar una excepción, esta rutina recibe un valor que se asigna a la variable especial $@, así que podríamos hacer un programa que lanza una excepción así:

 1 use Modern::Perl;
 2 use IO::File;
 3 
 4 eval {
 5     my $fh = IO::File->new("AlgunArchivo.txt", "r");
 6     die("No se puede abrir") unless $fh;
 7 };
 8 if ( $@ ) {
 9     say "Ooops: $@";
10 }

Para algunos esta forma de capturar excepciones puede parecer arcáica, sin embargo, es tan buena como cualquier otra y con las facilidades de Perl podríamos utilizarla como base para implementar una estructura similar a la de otros lenguajes, es decir algo como try/catch. Como ya he comentado en otras oportunidades Perl es un lenguaje excelente para implementar nuevas características en base a las primitivas del lenguaje, y para divertirnos un rato podemos hacernos nuestra propia versión de try/catch:

 1 use Modern::Perl;
 2 use IO::File;
 3 
 4 sub try(&) {
 5     eval { shift->() };
 6 }
 7 
 8 sub catch(&) {
 9     if ( $@ ) {
10         local $_ = $@;
11         shift->();
12     }
13 }
14 
15 try {
16     my $fh = IO::File->new( "AlgunArchivo.txt", "r" );
17     die("No se puede abrir") unless $fh;
18 };
19 catch {
20     say "Ooops: $_";
21 };

Aquí el prototipo & de Perl permite hacer que las subrutinas try y catch reciban una clausura, pero el prototipo permite eliminar la declaración sub, aparentando que try y catch son estructuras de control que tiene bloque de código asociado.

Como en realidad son subrutinas cuyo primer parámetro es una clausura, se pueden invocar y así en la línea 5 se extrae el primer argumento (con shift) y se ejecuta la calusura (con ->()), todo dentro de un eval, si ocurre alguna excepción, se aborta el eval y se termina el try.

Cuando se usa catch después de un try, cualquier valor de $@ se localiza en $_ y se ejecuta la clausura, en la cual se puede usar $_ como valor de la excepción.

Para hacer una extensión que permita utilizar las primitivas recién creadas, solo tenemos que hacer un nuevo módulo, al que llamaré MyTryCatch y que debe estar en el archivo "MyTryCatch.pm":

 1 package MyTryCatch;
 2 
 3 use Exporter;
 4 
 5 our $VERSION = "1.000";
 6 our @EXPORT_OK = qw( try catch );
 7 our @EXPORT = @EXPORT_OK;
 8 
 9 sub try(&) {
10     eval { shift->() };
11 }
12 
13 sub catch(&) {
14     local $_ = $@;
15     shift->();
16 }
17 
18 1;

Luego cada vez que necesite usar la nueva estructura de control solo tengo que incluirla en un programa, por ejemplo:

 1 package MyTryCatch;
 2 
 3 use Exporter;
 4 
 5 our $VERSION = "1.000";
 6 our @EXPORT_OK = qw( try catch );
 7 our @EXPORT = @EXPORT_OK;
 8 
 9 sub try(&) {
10     eval { shift->() };
11 }
12 
13 sub catch(&) {
14     if ( $@ ) {
15         local $_ = $@;
16         shift->();
17     }
18 }
19
20 1;

Las primitivas de que acabamos de crear tienen algunos defectos, por ejemplo puede usarse catch sin try, y una instrucción return dentro de un bloque try o catch, se sale del bloque y no de la subrutina donde se declara la estructura de captura de excepciones, entre otros. Sin embargo con algo más de esfuerzo podríamos hacer una extensión que declare una estructura que se comporte mejor.

En el CPAN hay varios módulos que permiten manejar errores con estructuras similares, desde los más sencillos como Try::Tiny, que sufre de algunos de los inconvenientes de MyTryCatch hasta los más complejos como TryCatch que usa magia de la buena como Devel::Declare para hacer una estructura de manejo de excepciones con casi cualquier cosa que se te pueda imaginar.

Si tus requerimientos no son muy exigentes mi recomendación es utilizar Try::Tiny, es realmente minúscula, casi no tiene dependencias y es muy fácil de instalar, por otra parte si quieres un sistema de manejo de excepciones que hace de todo, no te importa mucho el consumo de recursos y tienes la paciencia para instalar docenas de módulos, puedes usar TryCatch.

No hay comentarios:

Publicar un comentario