martes, 15 de septiembre de 2009

Usando Perl Moderno

[English translation]
Voy a intentar escribir una serie de artículos sobre Perl donde se pueda apreciar lo fácil y rápido que se pueden crear soluciones en esta plataforma.
Para ello elegí un diseño simple que me permite ilustrar una cantidad de técnicas y mejores prácticas, con un algoritmo accesible para cualquier programador aunque sea novato.
El programa de ejemplo será una calculadora estadística que en primera instancia utilizará un estilo tradicional, pero que se transformará poco a poco, haciéndose más flexible y fácil de mantener, mientras se aplican algunos mecanismos únicos del lenguaje y algunas librerías del CPAN.
El gran final es hacer que la calculadora sea una aplicación web utilizando un mecanismo sorprendente desarrollado sobre Perl. Una vez dicho esto comenzaré a usar Perl Moderno.
Haciendo honor al título del artículo, lo primero que hace nuestro programa es usar el módulo Modern::Perl, que es un atajo para decir:

use feature ':5.10';
use strict;
use warnings;
use mro 'c3';
Es decir, activa todas las características introducidas en Perl 5.10, también activa el modo estricto y las advertencias, y finalmente activa el orden de resolución de métodos utilizando el algoritmo C3. Como es de esperarse todos los ejemplos que veremos a lo largo de esta serie de artículos, solo funcionarán en Perl 5.10, porque intentare promover la mayor cantidad de características de esta nueva versión del lenguaje, así que: a instalar Perl 5.10.
Al usar Perl Moderno, se recomienda enfáticamente el uso de strict porque captura muchos errores comunes, entre ellos el uso accidental de referencias simbólicas, y los errores de tipográficos en las variables, al costo de que ahora deben ser declaradas con our (globales) ó my (léxicas), antes de usarse.
Por otra parte las advertencias permiten que perl nos informe acerca de posibles errores en la codificación. En perl 5.10 strict es más estricto y warnings tiene muchas advertencias nuevas, así que capturan más problemas que antes, lo que suele mejorar la calidad general del código y ahorrar tiempo de depuración.
En mi caso particular, cuando quise leer un comando y terminar el ciclo en caso de un fin de archivo escribí:

my $comando = readline(STDIN) or last;
Perl inmediatamente me advirtió que en algún caso podría confundirse undef (que denota el fin de archivo) con un "0" (cero), porque en perl "0" y undef se interpretan como falso. Una manera de escribir correctamente la instrucción sería:

defined (my $comando = readline(STDIN)) or last;
Pero aproveché para utilizar el nuevo operador // (defined or), con el que puedo escribir simplemente:

my $comando = readline(STDIN) // last;
El orden de resolución de métodos C3, resuelve algunos problemas existentes con el orden de resolución original de Perl, y lo recomendable es usarlo siempre en el nuevo código, esto no es del todo nuevo, hay módulos que usan este orden de resolución desde hace unos 4 años, solo que antes era un módulo del CPAN (Class::C3) y ahora tiene soporte nativo en el lenguaje.
Así que el primer consejo es usar Modern::Perl, porque activa una cantidad de características útiles y recomendadas de Perl.
Volviendo al programa, lo siguiente después de usar Modern::Perl es importar la subrutina looks_like_number() de Scalar::Util, que además de ahorrarme el trabajo de escribir las expresiones regulares para reconocer números, ahorra una buena cantidad de pánico de los lectores que podrían bloquearse de solo ver esas expresiones regulares.
El último módulo que se usa es el ingrediente principal de la calculadora, porque nunca pasó por mi mente escribir algoritmos de estadística, para eso existe el CPAN, que tiene de todo. En este caso usé Statistics::Descriptive, que sirve perfectamente a mi propósito.
En la línea 7 se declara una constante con un mensaje de error y en la 9 se define una variable con un objeto de la clase Statistics::Descriptive::Full, que será el estado de nuestra calculadora estádística, durante el ciclo de interpretación.
El ciclo principal es simple: lee un comando o se termina (last) si llegó el fin de archivo [línea 12], seguidamente se eliminan los espacios por la izquierda y la derecha del comando [línea 13], si el comando es un número se agrega al conjunto de datos del objeto Statistics::Descriptive::Full [línea 15] y si no, se selecciona y ejecuta un comando del interpretador.
La selección se hace con la nueva estructura de control de Perl 5.10 given/when [líneas 18-36] que efectúa smart matching entre el valor dado (given) y las clausulas de comparación (when). Como la comparación es "inteligente" depende de los operandos, y en general funciona como uno se lo imagina, sin embargo hay algunos casos rebuscados y nunca está de más leer los manuales.
Finalmente el nuevo operador say, que no es más que un print que emite un fin de línea, ayudando a evitar un montón de concatenaciones con "\n" y por ello contribuye con la claridad del código.

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use Scalar::Util qw( looks_like_number );
 5 use Statistics::Descriptive;
 6 
 7 use constant SYNTAX_ERROR => "Error: tipee 'help' para ayuda";
 8 
 9 my $s = Statistics::Descriptive::Full->new();
10 while (1) {
11     print "Listo> ";
12     my $command = readline(STDIN) // last;
13     $command =~ s/^\s+//; $command =~ s/\s+$//;
14     if ( looks_like_number($command) ) {
15         $s->add_data($command);
16     }
17     else {
18         given ($command) {
19             when ("sum")                { say "$command = " . $s->sum() }
20             when ("mean")               { say "$command = " . $s->mean() }
21             when ("count")              { say "$command = " . $s->count() }
22             when ("variance")           { say "$command = " . $s->variance() }
23             when ("standard_deviation") { say "$command = " . $s->standard_deviation() }
24             when ("min")                { say "$command = " . $s->min() }
25             when ("mindex")             { say "$command = " . $s->mindex() }
26             when ("max")                { say "$command = " . $s->max() }
27             when ("maxdex")             { say "$command = " . $s->maxdex() }
28             when ("sample_range")       { say "$command = " . $s->sample_range() }
29             when ("median")             { say "$command = " . $s->median() }
30             when ("harmonic_mean")      { say "$command = " . $s->harmonic_mean() }
31             when ("geometric_mean")     { say "$command = " . $s->geometric_mean() }
32             when ("mode")               { say "$command = " . $s->mode() }
33             when ("trimmed_mean")       { say "$command = " . $s->trimmed_mean() }
34             when (/^(exit|quit)$/)      {last}
35             default                     { say SYNTAX_ERROR }
36         }
37     }
38 }
Para usar la calculadora simplemente ejecutamos el archivo, aquí les muestro una corrida de prueba:

opr@toshi$ perl stat.pl
Listo> 19
Listo> 45
Listo> 24
Listo> 15
Listo> 39
Listo> 48
Listo> 36
Listo> count
count = 7
Listo> 10
Listo> 28
Listo> 30
Listo> count
count = 10
Listo> mean
mean = 29.4
Listo> standard_deviation
standard_deviation = 12.685950233756
Listo> salir
Error: tipee 'help' para ayuda
Listo> help
Error: tipee 'help' para ayuda
Listo> exit
opr@toshi$

Una mejora sencilla

Una mejora de estilo podría ser eliminar el if de la línea 15 y hacer la comparación en el given, esto además me permite mostrar que given topicaliza $_ al valor dado y que las clausulas when no solo comparan cadenas (usando eq) y expresiones regulares (usando =~), sino que permiten, entre otros, escribir expresiones booleanas utilizando $_ como un alias al valor dado en el given.

 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use Scalar::Util qw( looks_like_number );
 5 use Statistics::Descriptive;
 6 
 7 use constant SYNTAX_ERROR => "Error: tipee 'help' para ayuda";
 8 
 9 my $s = Statistics::Descriptive::Full->new();
10 while (1) {
11     print "Listo> ";
12     my $command = readline(STDIN) // last;
13     $command =~ s/^\s+//; $command =~ s/\s+$//;
14     given ($command) {
15         when ( looks_like_number($_) ) { $s->add_data($command) }
16         when ("sum")                   { say "$command = " . $s->sum() }
17         when ("mean")                  { say "$command = " . $s->mean() }
18         when ("count")                 { say "$command = " . $s->count() }
19         when ("variance")              { say "$command = " . $s->variance() }
20         when ("standard_deviation")    { say "$command = " . $s->standard_deviation() }
21         when ("min")                   { say "$command = " . $s->min() }
22         when ("mindex")                { say "$command = " . $s->mindex() }
23         when ("max")                   { say "$command = " . $s->max() }
24         when ("maxdex")                { say "$command = " . $s->maxdex() }
25         when ("sample_range")          { say "$command = " . $s->sample_range() }
26         when ("median")                { say "$command = " . $s->median() }
27         when ("harmonic_mean")         { say "$command = " . $s->harmonic_mean() }
28         when ("geometric_mean")        { say "$command = " . $s->geometric_mean() }
29         when ("mode")                  { say "$command = " . $s->mode() }
30         when ("trimmed_mean")          { say "$command = " . $s->trimmed_mean() }
31         when (/^(exit|quit)$/)         {last}
32         default                        { say SYNTAX_ERROR }
33     }
34 }
Pienso que casi cualquier programador habituado a lenguajes dinámicos como Python o Ruby puede comprender sin dificultad código en Perl Moderno y sentirse cómodo trabajando con este.
Los programadores de lenguajes como C, C++,C# o Java, después de adaptarse a algunos conceptos deberían sentir una especie de experiencia liberadora, porque seguramente en cualquiera de ellos cuesta mucho más escribir un programa como este.
En el próximo artículo veremos algunas características dinámicas de Perl, que hacen el programa más corto, flexible y fácil de mantener.

7 comentarios:

  1. given ($command) {
    when (/sum|mean|count|etc/) {
    say "$command = ".$s->$command;
    }
    ...
    }

    ResponderEliminar
  2. fix: /^sum|mean|count|etc$/ :)

    ResponderEliminar
  3. en realidad /^(sum|mean|count|etc)$/, pero hay mejores maneras, ya verás ;-)

    ResponderEliminar
  4. like this?: ;)

    given ($command) {
    when ($s->can($command)) {
    say "$command = ".$s->$command;
    }
    ...
    }

    ResponderEliminar
  5. Si, esa es una manera, aunque tiene un problema de seguridad, puedes identify it?.

    Yes that's one way to do it, although it has a security problem, can you identify it?.

    ResponderEliminar
  6. It is a shell script, so I don't see any security problem, except maybe that user can call DESTROY on $s or something alike.

    So:

    when ($c->can(lc($command))) {..

    :)

    ResponderEliminar
  7. Maybe not in this specific example, but there is a problem with using unverified user input as a dispatch mechanism, I am trying to give good advice for newcomers and rookies, so I mush warn about this.

    ResponderEliminar