domingo, 27 de septiembre de 2009

Calculadora Estadística: Consola Amigable

[English translation]
Una de las características que hacen agradable el uso de una aplicación de consola es la capacidad de editar la linea de comandos y de reutilizar los comandos anteriores, estas dos características hacen la aplicación mucho más amigable.  Así que vamos a agregarlas a la calculadora del artículo anterior.
Este es uno de esos trabajos donde el CPAN demuestra porque es la mejor ventaja de Perl, voy a usar Term::ReadLine, una interfaz unificada para lectura de consola, esta librería permite el uso de varios backends que son los que implementan la funcionalidad, y para que nuestro ejemplo funcione yo instalé Term::ReadLine::Perl, aunque supongo que Term::ReadLine::Gnu debería funcionar igual. Ambas son interfaces de la librería readline(3) que usan muchas aplicaciones, incluyendo bash.
Para tener una interfaz similar a la de bash solo tenemos que cambiar las líneas 27 y 28 de nuestro programa anterior por:
28     my $command = $term->readline("Listo> ") // last;
como siempre, se debe declarar el uso del módulo, e inicializar el terminal creando un objeto $term que usaremos para invocar el método readline:
 7 use Term::ReadLine;
 8 
 9 my $term = new Term::ReadLine 'Statistic Calculator';
usamos Term::ReadLine y no alguna de las variantes específicas, porque este se encarga de ejecutar las versiones específicas de manera automática, aunque permite también que el usuario mantenga control en caso de ser necesario. La librería tiene funciones que permiten adornar el prompt, autocompletar y algunas otras cosas, de nuevo el CPAN salva el día sin hacer más que leer la documentación de un módulo.
Voy a aprovechar para reparar algunas otras cosillas que andan molestando por allí (léase bugs), primero el más fastidioso y fácil de resolver: cuando damos un comando vacío sale un mensaje de error, porque una línea vacía no es capturada por ninguna clausula when, así que el remedio es fácil, agregamos:
when ("")  {  } # si el comando es vacío no hacer nada
y listo.
Otro problema más difícil de resolver son los comandos que no devuelven nada, como "clear", que nos dan un advertencia, gracias al use warnings implícito en Modern::Perl:
Listo> clear
Use of uninitialized value in concatenation (.) or string at calc1.pl line 32.
clear =
Para resolver este problema voy a crear una subrutina que aplique las funciones e imprima el resultado, para mantener el ciclo de despacho lo más claro posible, así que:
32   when (%FUNCS)  { say "$command = " . $s->$command }
pasará a ser esto:
32   when (%FUNCS)  { apply $command }
y luego implementaremos la subrutina apply, que recibe un comando, lo ejecuta y captura el resultado, pero se retorna a menos que el resultado esté definido y no sea una cadena vacia:
26     return unless defined $result and $result ne "";
La calculadora ahora esvmás amigable, sin embargo todavía tiene problemas, si ejecutan trimmed_mean se darán cuenta de que da un error, en el manual está la causa, la función trimmed_mean recibe 1 o 2 parámetros, pero por ahora la calculadora solo acepta comandos sin parámetros, en el próximo artículo veremos como agregarle parámetros y también desplegar resultados no escalares, como arreglos y hashes.
Nuestro programa completo ahora luce así:
 1 #!/usr/bin/perl
 2 
 3 use Modern::Perl;
 4 use Scalar::Util qw( looks_like_number );
 5 use Statistics::Descriptive;
 6 use Pod::Perldoc;
 7 use Term::ReadLine;
 8 
 9 my $term = new Term::ReadLine 'Statistic Calculator';
10 
11 my %FUNCS = map { $_ => 1 } qw( sum mean count variance standard_deviation
12     min mindex max maxdex sample_range median harmonic_mean geometric_mean
13     mode trimmed_mean clear );
14 
15 my @COMMANDS = qw( exit quit help man );
16 
17 sub help { say "Comandos: " . join( ", ", sort @COMMANDS, keys %FUNCS ) }
18 
19 sub man { Pod::Perldoc->new(args => \@_)->process }
20 
21 my $s = Statistics::Descriptive::Full->new();
22 
23 sub apply {
24     my $command = shift;
25     my $result = $s->$command;
26     return unless defined $result and $result ne "";
27     say "$command = $result";
28 }
29 
30 while (defined(my $command = $term->readline("Listo> "))) {
31     $command =~ s/^\s+//; $command =~ s/\s+$//;
32     given ($command) {
33         when ( looks_like_number($_) ) { $s->add_data($command) }
34         when (%FUNCS)                  { apply $command }
35         when ("man")                   { man "Statistics::Descriptive" }
36         when ( [ "exit", "quit" ] )    { last }
37         when ("help")                  { help }
38         when ("")                      { }
39         default                        { say "Error: tipee 'help' para ayuda" }
40     }
41 }

No hay comentarios:

Publicar un comentario