Mostrando entradas con la etiqueta perl. Mostrar todas las entradas
Mostrando entradas con la etiqueta perl. Mostrar todas las entradas

lunes, 7 de marzo de 2016

Programar en un entorno orientado a eventos con Perl

POE, un entorno orientado a eventos

Si ya has programado una aplicación gráfica usando algo como Tk o Gtk, sabrás que es un poco diferente de la programación procedural diaria. En la programación normal, se escribe una secuencia de cosas que quieres que el programa haga y éste lo hace. Sin embargo, en las GUI's no se trabaja de ésta manera. Se setea un entorno (por ejemplo una ventana) que responde a ciertos eventos (un click de un botón o la selección de un ítem en un menú). A eso se le llama Paradigma Orientado a Eventos.
No sólo se usa en GUI's. Por ejemplo, un servidor en una red no realiza una secuencia de eventos, sino que se sienta a esperar una conexión (un evento) y entonces le sirve a esa conexión según el input del cliente. Cuando el cliente termina y se desconecta, el servidor vuelve a esperar por un próximo evento.
En forma similar se podría escribir un script que mira una carpeta; el script se sienta a mirar y periódicamente busca en los archivos de la carpeta, y cuando detecta cambios dispara una respuesta o realiza algunas acciones.
El núcleo del paradigma orientado a eventos es el bucle principal, a veces llamado 'main loop'. Tk tiene uno, el módulo Event tiene uno, y POE, un entorno orientado a eventos, tiene uno. El bucle principal de POE es manejado por el kernel POE.
POE puede ser pensado como un diminuto sistema operativo que tiene un kernel. Cuando el kernel de un sistema operativo termina de asignar los trabajos en su entorno, se sienta a esperar por nuevos eventos. Éstos pueden ser llamadas al sistema desde el espacio del usuario o interrupciones de hardware. Además de manejar eventos se ocupa también del pasaje de mensajes entre los diferentes componentes, típicamente comunicación entre procesos (IPC).
El kernel POE también sirve a eventos y maneja la comunicación entre las diferentes partes del mundo de POE, aunque su equivalente de los procesos son llamados Sessions.

Hola mundo, POE

Mucha charla y nada de código, rectifiquemos con un breve ejemplo:
    #!/usr/bin/perl
    use strict;
    use warnings;
    use POE;
    POE::Session->create(
        inline_states => {
        _start  => \&start,
        hello   => \&hello,
        },
    );

    print "Running Kernel\n";
    $poe_kernel->run(  );
    print "Exiting\n";
    exit(0);

    sub start {
        my ($kernel) = $_[KERNEL];
        print "Setting up a session\n";
        $kernel->yield("hello");
    }

    sub hello { print "Hola Mundo!\n"; }

Este es el equivalente POE del famoso programa Hola Mundo. Si continuamos con la analogía del sistema operativo (una analogía poco útil pero por ahora la usaremos) entonces iniciamos el kernel de la máquina y creamos un único proceso que imprime "Hola Mundo!" y finaliza.

    use POE;
    print "Running Kernel\n";
    $poe_kernel->run(  );
    print "Exiting\n";
    exit(0);

Aquí está el núcleo de cualquier programa POE, la variable $poe_kernel es provista por el módulo POE y representa el kernel mismo. La llamada a run() en muchos casos no retorna nunca, por ejemplo un servidor que espera en un loop por nuevas conexiones. En nuestro caso, sin embargo, sólo seteamos una pequeña sesión que termina en seguida. En códigos más nuevos se prefiere usar POE::Kernel->run(), en vez de la variable global.

    POE::Session->create(
        inline_states => {
        _start  => \&start,
        hello   => \&hello,
        },
    );

En esta parte se crea una sesión. Una sesión puede ser pensada como una máquina de estados con múltiples estados, o como un manejador de múltiples eventos, las dos representaciones son equivalentes. Hablando de estados, el ejemplo anterior define dos estados en el parámetro inline_states que se pasa al constructor. Los estados cuyos nombres empiezan con guión bajo son predefinidos por POE, mientras que los otros son definidos por el usuario. La sesión entra al estado _start automáticamente después de ser construída.
Hablando en términos de eventos, decimos que nuestra sesión responde al evento _start y al evento hello y que POE envía un evento _start a la sesión tan pronto como ésta es creada.
Hay otros eventos predefinidos, la mayoría de ellos son para hacer relaciones padre-hijos y señales. Está el evento _stop que es enviado cuando la sesión debe finalizar. Veamos cómo se define un manejador de eventos:

    sub start {
        my ($kernel) = $_[KERNEL];
        print "Setting up a session\n";
        $kernel->yield("hello");
    }

    sub hello { print "Hola Mundo!\n"; }

Se le pasa a nuestro manejador start() un número de parámetros, uno de los cuales es un manejador del kernel POE. Se extrae éste de la lista de parámetros usando la constante KERNEL. En aras de la eficiencia, POE utiliza constantes para indexar la variable @_ en vez de un parámetro hash. A menudo se verán manejadores que comienzan con algo como esto:

    my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];

Esto es un ordinario array slice, con índices constantes. Retornan el kernel POE, la heap y el objeto sesión actual. La heap es un lugar donde la sesión puede almacenar su privado stuff. Volveremos a esto para ver qué tipo de stuff es bueno almacenar en la heap más tarde.
Ahora que tenemos el kernel, qué hacemos con él? Bien, le decimos que queremos pasar a otro estado, el estado hello:

    $kernel->yield("hello");

Usamos yield() para enviar un evento a la sesión actual; si tuviéramos almacenada otra sesión, podríamos comunicarnos con ella enviándole un evento usando el método post(). Veremos un ejemplo más adelante.
Por ahora le dijimos al kernel POE que queremos pasar al estado hello. Pero esto no sucederá hasta que POE corra en su bucle de eventos. Una vez que corre en su bucle con $poe_kernel->run(), el kernel mira en su lista de tareas pendientes, encuentra que la primer cosa para hacer es pasar nuestra sesión al estado hello y dispara el manejador apropiado. Entonces se imprime el mensaje "Hola Mundo!".

Hola de nuevo, POE!

Supongamos que ahora queremos repetir el mensaje cada 5 segundos. Podríamos lograrlo:

    sub hello {
        my ($kernel) = $_[KERNEL];
        print "Hola Mundo!\n";
        sleep 5;
        $kernel->yield("hello");
    }

Funciona, pero no es la manera de comportarse en un entorno cooperativo y multitarea. No podemos colgar el kernel entero por 5 segundos porque otras sesiones podrían tener cosas que hacer: por ejemplo en una red que necesita servicios, etc. En vez, permitimos que el kernel maneje el estado hello dentro de 5 segundos en el futuro. Para hacerlo usamos el método delay_set() del kernel:

    sub hello {
        my ($kernel) = $_[KERNEL];
        print "Hola Mundo!\n";
        $kernel->delay_set("hello", 5);
    }

Nota mental: no usar sleep() dentro de POE porque pueden ocurrir situaciones indeseables.
Ahora seremos más amables. Veamos como podemos hacer con dos sesiones diferentes corriendo.
Se trata de un código ligeramente modificado del maravilloso tutorial de POE de Matt Sergeant:

    use POE;

    for my $session_no (1..2) {
      POE::Session->create(

        inline_states => {
          hello => \&hello,
          _start => sub { $_[KERNEL]->alias_set("session_" . $session_no) },
      });
    }

    $poe_kernel->post("session_1", "hello", "session_2");
    $poe_kernel->run(  );
    exit(0);

    sub hello {
      my ($kernel, $session, $next) = @_[KERNEL, SESSION, ARG0];
      print "Event in ", $kernel->alias_list($session), "\n";
      $kernel->post($next, "hello", $session->ID);
    }

Ambas sesiones van ejecutando hello() en forma alternada, y para lograr esto una sesión le pide a la otra que se ejecute y viceversa.
Veamos con más detalle; creamos las sesiones en un bucle (en este caso dos) que tienen un manejador start() y un manejador para el evento hello(). Las sesiones comparten el código para sus dos manejadores, pero los argumentos que se les pasan serán distintos en cada caso.
Esta vez el manejador start() hace algo un poco diferente del script anterior. Le dice al kernel que registre un alias para la sesión actual. Cada sesión tiene un ID interno (que se usa más tarde en el script) pero que solamente conoce POE cuando crea las sesiones. Registrando un alias amigable para el programador nosotros obtenemos una manera para referirnos a la sesión más adelante. Esta vez start() no hace un yield().
Nuevamente para ser amigables con el programador, le pedimos al kernel cuál es el alias de nuestra sesión para mostrarla en un mensaje:

    print "Event in ", $kernel->alias_list($session), "\n";

Ahora que hay más de una sesión necesitamos decirle al kernel cual de ellas comenzará la acción, por eso hacemos un post al evento hello() de la primera sesión, llamándola por su alias. El tercer parámetro es un argumento más del post, en este caso le pasamos el alias de la segunda sesión:

    $poe_kernel->post("session_1", "hello", "session_2");

Cuando hacemos yield() o posteamos eventos, podemos pasar parámetros adicionales al evento, los cuales pasan al manejador del evento. Estos argumentos llegan en la variable @_ comenzando en la posición ARG0. Si tenemos muchos argumentos, podríamos escribir algo como esto para tomarlos a todos:

      my ($kernel, $session, @args) = @_[KERNEL, SESSION, ARG0..$#_];

Pero aquí nos interesa solamente el primer argumento, que es el nombre de la próxima sesión a llamar. La sesión 1 le pasa el control a la sesión 2 y viceversa. Ahora que empieza a correr no necesitamos ser amigables con el programador, entonces identificamos a la sesión por su ID interno:

    $kernel->post($next, "hello", $session->ID);

Está diciendo: "Yo te estoy llamando a tí ahora, y la próxima vez llamame a mí (por mi ID)".
Con estas dos sesiones corriendo, tenemos un entorno cooperativo y multitarea:

    Event in session_1
    Event in session_2
    Event in session_1
    Event in session_2

    ...
Sin embargo, si vamos a hacer algo interesante con este entorno, tenemos que comenzar a mirar qué nos trae POE para más complejas acciones de I/O.

Wheels

Los Wheels son la fuerza de arrastre (ha, ha!) del sistema de I/O de POE. Un wheel es una conexión al mundo exterior que genera los eventos. Miremos los wheels como un equivalente de los manejadores de ficheros, pero son más que eso.
El más simple wheel para entender es POE::Wheel::FollowTail, el cual sigue de cerca a un archivo que está creciendo. Se le pasa un nombre de archivo y el wheel genera eventos cuando el archivo tiene actualizaciones. Veamos un ejemplo cortito:
    use POE qw(Wheel::FollowTail);

    POE::Session->create(
      inline_states => {
         _start => sub {
            my ($heap) = $_[HEAP];
            my $log_watcher = POE::Wheel::FollowTail->new(
                Filename => "my_log_file.txt",
                InputEvent => "got_record",
            );

            $heap->{watcher} = $log_watcher;
         },
         got_record => sub { my $record = $_[ARG0]; print $record,"\n"; }
      }
    );

    $poe_kernel->run(  );

Primero, notemos la forma compacta de cargar múltiples módulos POE; cualquier parámetro pasado a use POE será interpretado como nombres de módulo bajo POE:: y serán usados.
Como antes, tenemos dos estados. El estado got_record es bonito y fácil de enteder: imprime su argumento. Miremos el estado _start con más detalle:

            my $log_watcher = POE::Wheel::FollowTail->new(
                Filename => "my_log_file.txt",
                InputEvent => "got_record",
            );

El trabajo del evento start es setear nuestro wheel. Le decimos que mire al archivo my_log_file.txt y que postee el evento got_record cada vez que vea una nueva línea.
Y qué hacemos con nuestro wheel? Nosotros queremos que el wheel persista por la duración de la sesión sino sería algo inútil, al ser un objeto ordinario de Perl será destruído al finalizar el bloque de ejecución actual sino lo almacenamos en algún lado. Por eso tener un área de almacenamiento por sesión es muy valioso, la heap:

            my ($heap) = $_[HEAP];
            . . .
            $heap->{watcher} = $log_watcher;

Eso es todo lo que necesitamos; el wheel se sienta a mirar el archivo y va generando los eventos, nuestro manejador imprime las líneas que ha visto. Ahora agregaremos otro wheel en la ecuación.
Nota: el script anterior pierde el primer carácter de la línea aunque no sea un "\n"?
Supongamos por alguna razón que nuestro archivo de log es en realidad un log con datos binarios y queremos imprimir las líneas en exadecimal usando el comando hexdump.
Si no tienes el comando hexdump lo puedes imitar creando tu propio script en Perl, como el siguiente y llamarlo hexdump:

my $i = -16;
binmode(STDIN);
my $data; $|++;
printf "%07x ". ("%02x%02x "x8)."\n", $i+=16, map ord, split//,$data
    while read STDIN, $data, 16;

El POE::Wheel::Run manejará I/O apoyado en programas externos. Podemos crear un wheel que llame a hexdump y enviarle los datos que queramos:

    use POE qw(Wheel::FollowTail Wheel::Run);

    POE::Session->create(
      inline_states => {
         _start => sub {
            my ($heap) = $_[HEAP];
            my $log_watcher  = POE::Wheel::FollowTail->new(
                Filename     => "my_log_file.txt",
                InputEvent   => "redirect",
            );
            my $dumper = POE::Wheel::Run->new(
                Program      => "/usr/bin/hexdump",
                StdoutEvent  => "print"
            );

            $heap->{watcher} = $log_watcher;
            $heap->{dumper}  = $dumper;
         },
         redirect => sub {
            my ($heap, $data) = @_[HEAP, ARG0];
            $heap->{dumper}->put($data);
         },

         print => sub { my $record = $_[ARG0]; print $record, "\n"; }
      }
    );
    $poe_kernel->run(  );

Miremos lo que está sucediendo:


El wheel FollowTail obtiene sus datos y los envía a la sesión, la cual los envía al wheel Run, éste genera un evento print e imprime esos datos. Maravilloso.
Excepto que no funciona. Si ejecutamos el programa con el comando hexdump de Linux, todos nuestros datos desaparecen en el éter y nunca los verás de nuevo. Pero hay algo interesante: si usamos el hexdump echo en Perl, el programa trabaja bien. Cómo es posible?
La clave es el mágico $|++ de nuestra versión. El hexdump del sistema almacena toda la salida si siente que está conectado a un pipe. Como nuestro programa nunca termina, hexdump sólo se queda almacenando datos hasta que nosotros cortamos la ejecución, y entonces todo se pierde. Necesitamos que el comando hexdump piense que está conectado con un terminal real. Por supuesto, POE provee una manera para hacer esto:

            my $dumper = POE::Wheel::Run->new(
                Program      => "/usr/bin/hexdump",
                Conduit      => "pty",
                StdoutEvent  => "print"
            );

Hay otros wheels que pueden trabajar juntos como este: POE::Wheel::Curses lee datos usando una librería no bloqueante de interface Curses, mientras que POE::Wheel::ReadLine usa Term::ReadKey para implementar una interface de consola de entrada basada en líneas. POE::Wheel::ListenAccept es un socket de bajo nivel que se queda escuchando. Veremos a continuacón dos de los más importantes wheels en el próximo ejemplo: POE::Wheel::ReadWrite y POE::Wheel::SocketFactory.

Despachador de puertos

Ya sabes. Estás en el trabajo, detrás de un agresivo firewall que no te permite usar el IRC. Y tú no puedes trabajar sin tu IRC, así que realizas una tramolla algo sucia para conectarte. Seteas un reenvío de puerto para que cuando se intenta conectar al puerto 6667 en la máquina local, sea llevado al puerto 80 (el cual está permitido en tu firewall) en tu hosted box en el mundo real. Entonces otro despachador escuchará en el puerto 80 de esa máquina y enviará las conexiones a través del puerto 6667 al servidor IRC. Luego seteas el cliente IRC para que se conecte a localhost y voilà, estás conectado! Veamos como POE puede ayudarte a perder tu empleo.
Comenzaremos seteando un servidor que escuche conexiones:

        my $office = shift;

        my ($local_address, $local_port, $remote_address, $remote_port);
        ($office ? $remote_address : $local_address) = "mybox.real-world.int";
        ($office ? $local_port     : $remote_port)   = 6667;
        ($office ? $remote_port    : $local_port)    = 80;

        if ($office) {
           $local_address = "127.0.0.1";
        } else {
           $remote_address = "irc.perl.org";
        }

        POE::Session->create(
          inline_states => {
           _start => \&server_start,
            client_connected => \&client_connected,
            on_server_error => \&server_error
          },
          args =>
            [ $local_address, $local_port, $remote_address, $remote_port ]
          );
       $poe_kernel->run;

Una vez que sabemos que despachamos desde la oficina a la máquina hosted o desde la máquina hosted a la oficina, seteamos varias direcciones y puertos en la forma apropiada y creamos una nueva sesión con esos parámetros. Esta sesión iniciará todas las sesiones que necesitemos. Como estamos tratando con tres partes que se unen en este reenvío de puertos vamos a necesitar 3 sesiones y 3 wheels.
Omitiremos el manejo de errores para hacer más clara la explicación. Además en caso de error es poco lo que se puede hacer, más que ignorar y esperar que la próxima conexión sea exitosa.
El primer wheel aparece en el estado start() y tiene que escuchar por el puerto y dirección adecuados, utilizaremos el SocketFactory wheel:

    sub server_start {
        my ( $heap, $local_addr, $local_port, $remote_addr, $remote_port )
        = @_[ HEAP, ARG0,        ARG1,        ARG2,         ARG3 ];

        # Store our parameters
        $heap->{local_addr}  = $local_addr;
        $heap->{local_port}  = $local_port;
        $heap->{remote_addr} = $remote_addr;
        $heap->{remote_port} = $remote_port;

        # Create and store a wheel
        $heap->{server_wheel} = POE::Wheel::SocketFactory->new
          ( BindAddress  => $local_addr,
            BindPort     => $local_port,
            Reuse        => 'yes',
            SuccessEvent => 'client_connected',
            FailureEvent => "on_server_error",
          );
    }

Cuando el wheel SocketFactory acepta una conexión y postea un evento cliente_connected(), pasa el socket y el peer address y el puerto así:

    sub client_connected {
        my ( $heap, $socket, $peer_addr, $peer_port ) =
          @_[ HEAP,  ARG0,    ARG1,       ARG2];
    }

Ahora tenemos un servidor que escucha y acpeta conexiones, pero que hacemos una vez que acepta una? Por lo común, una aplicación no POE, probablemente haría un fork o un hilo para servir el requerimiento y luego volvería a escuchar por nuevas conexiones. En cambio en POE, creamos una nueva sesión para manejar al cliente. Recuerden que hemos almacenado los parámetros de nuestra conexión en la heap de la primera sesión, entonces podemos pasárselos a la nueva sesión:

    sub accept {
        my ( $heap, $socket, $peer_addr, $peer_port ) =
          @_[ HEAP,  ARG0,    ARG1,       ARG2];

        POE::Session->new
          ( _start => \&forwarder_start,
            server_connect => \&connected_to_other_side,
            client_input   => \&forward_outbound,
            server_input   => \&forward_inbound,

            [ $socket, $peer_addr, $peer_port,
              $heap->{remote_addr}, $heap->{remote_port} ]
          );
    }

Cuando esta sesión inicia necesita configurar la conexión con el destino y estar listo para leer y escribir datos desde el cliente. Hacemos esto pasando al cliente $socket que recibimos para nuestro segundo wheel, POE::Wheel::ReadWrite, un wheel genérico de I/O de POE. Igual que en un entorno no POE, nosotros reusamos el socket que tenemos para manejar la conexión como lo hacemos con un filehandle para leer y escribir archivos.
Paremos un momento para mirar el siguiente diagrama:

Hasta ahora nos hemos ocupado del cliente que ha contactado con nosotros; también queremos otro wheel para conectar con el servidor en el otro extremo del túnel de reenvío:

    sub forwarder_start {
        my ( $heap, $session,
            $socket, $peer_host, $peer_port, $remote_addr, $remote_port
          ) =
          @_[ HEAP, SESSION, ARG0, ARG1, ARG2, ARG3, ARG4 ];

        ($heap->{peer_host}, $heap->{peer_port}, 
    $heap->{remote_addr}, $heap->{remote_port})=
                  ($peer_host, $peer_port, $remote_addr, $remote_port);

        $heap->{wheel_client} = POE::Wheel::ReadWrite->new
          ( Handle => $socket,
            Filter     => POE::Filter::Stream->new,
            InputEvent => 'client_input',
          );

        $heap->{wheel_server} = POE::Wheel::SocketFactory->new

          ( RemoteAddress => $remote_addr,
            RemotePort   => $remote_port,
            SuccessEvent => 'server_connect',
          );
    }

Hay un pequeño detalle; desde que intentamos ser lo más asincrónicos posible, tenemos que tener en cuenta el caso en que aún está estableciéndose la conexión con el servidor, pero ya tenemos datos enviados por el cliente. Agregaremos una cola para almacenar cualquier dato que tengamos antes de establecer la conexión:

        $heap->{state} = 'connecting';
        $heap->{queue} = [  ];

Ahora veamos qué sucede cuando los datos llegan del cliente. Si aún esperamos la conexión, los ponemos en cola. En otro caso los enviamos a través del otro wheel al servidor:

    sub forward_outbound {
        my ( $heap, $input ) = @_[ HEAP, ARG0 ];

        if ( $heap->{state} eq 'connecting' ) {
            push @{ $heap->{queue} }, $input;
        }
        else {
            $heap->{wheel_server}->put($input);
        }
    }

Una vez que hemos seteado la conexión con el otro lado, necesitamos hacer lo mismo de nuevo y colocar el socket en nuestro tercer wheel, otro ReadWrite wheel.

    sub connected_to_other_side {
        my ( $kernel, $session, $heap, $socket ) = @_[ KERNEL, SESSION,
    HEAP, ARG0
    ];

        $heap->{wheel_server} = POE::Wheel::ReadWrite->new
          ( Handle => $socket,
            Driver     => POE::Driver::SysRW->new,
            Filter     => POE::Filter::Stream->new,
            InputEvent => 'server_input',
          );
    }

Ahora podemos desencolar la cola en caso de que tenga datos pendientes:

        $heap->{state} = 'connected';
        foreach my $pending ( @{ $heap->{queue} } ) {
            $kernel->call( $session, 'client_input', $pending );
        }
        $heap->{queue} = [  ];

Por cada porción de datos que recibimos, posteamos los datos de regreso al evento client_input; sin embargo, esta vez no seguimos conectados, y el evento pasa los datos al servidor.
Finalmente, necesitamos mover los datos recibidos desde el servidor hacia el tunel al cliente, completando la función forward_inbound:

    my ( $heap, $input ) = @_[ HEAP, ARG0 ];
    $heap->{wheel_client}->put($input);

Miremos el diagrama final del despachador entero antes de pensar en cómo hacerlo más simple:

Sobre este tutorial

Se trata de una traducción libre del original "Programming in an Event-Driven Environment" escrito por Simon Cozens.



domingo, 14 de febrero de 2016

Introducción a POE para novatos


Sobre este texto

Este texto es una adaptación libre del texto original "A beginner's introduction to POE" de Dennis Taylor y Jeff Goff.

Qué es POE y porqué debería usarlo?

La mayoría de los programas que nosotros escribimos cada día tiene el mismo esqueleto básico: la inicialización, la realización de una serie de acciones y la finalización. Esto trabaja bien para programas que no necesitan mucha interacción con otros usuarios o datos, pero para tareas más complicadas, se necesita una estructura de programa más expresiva.
Ahí es donde POE (Perl Object Environment) viene a ayudar. POE es un framework para construir programas Perl que llevan en su naturaleza tareas que involucran reaccionar ante datos externos, tales como una red de comunicaciones o interfaces de usuario. Los programas escritos en POE son completamente no-lineales; se inicializa un conjunto de pequeñas subrutinas y se define cómo se llaman entre sí, y POE automáticamente va cambiando entre ellas mientras maneja la entrada y salida del programa. Puede ser confuso al principio, si estás acostumbrado a la programación procedural, pero con un poco de práctica se vuelve una segunda naturaleza.

Diseño POE

No es exagerado decir que POE es un pequeño sistema operativo escrito en Perl, con su propio kernel, procesos, comunicación entre procesos (IPC), drivers, etc. En la práctica, sin embargo, lo podemos describir como un simple sistema para ensamblar máquinas de estado. He aquí una breve descripción de cada una de las piezas que conforman un entorno POE:

Estados

El bloque de construcción básico de un programa POE es el estado, es la pieza de código a ser ejecutada cuando un evento ocurre, por ejemplo cuando arrivan datos o una sesión tiene cosas que hacer o cuando una sesión envía un mensaje a otra. Todo en POE está basado alrededor de recibir y manejar estos eventos.

El kernel

El kernel de POE es muy parecido al kernel de un sistema operativo: guarda la pista de cada proceso y dato que está detrás de escena y maneja cada pieza de tu código para que corra. Se puede usar el kernel para setear alarmas para los procesos POE, encolar estados que se quieran correr y realizar otros servicios varios de bajo nivel, pero casi nunca se interactúa con el kernel directamente.

Sesiones

Las sesiones son para POE el equivalente de los procesos en un sistema operativo real. Una sesión es sólo un programa POE con cambios de un estado a otro mientras ejecuta. Puede crear sesiones hijas, enviar eventos POE a otras sesiones, etc. Cada sesión puede almacenar datos específicos de la sesión en un hash llamado heap. La heap es accesible desde cada estado en a sesión. POE tiene un muy simple modelo multitasking cooperativo: cada sesión ejecuta en el mismo proceso SO sin threads ni forking. Por esta razón se debería cuidar de no usar system calls bloqueantes en los programas POE.
Estas son las piezas básicas de POE, sin embargo quedan unas pocas algo más avanzadas que explicaremos antes de ir a ver un código real.

Drivers

El más bajo nivel de la capa I/O de POE es Drivers. Actualmente hay sólo un driver incluído en la distribución POE (POE::Driver::SysRW lee y escribe datos desde un manejador de archivos) y no hay mucho para decir sobre él. Además, nunca se utiliza un driver directamente.

Filters

Por otro lado, Filters es extraordinariamente útil. Un filtro es una simple interface para convertir trozos de datos formateados hacia otro formato. Por ejemplo POE::Filter::HTTPD convierte requerimientos HTTP 1.0 en objetos HTTP::Request y viceversa, y POE::Filter::Line convierte un flujo de datos en una serie de líneas (parecido al operador <> de Perl).

Wheels

Los Wheels contienen piezas reutilizables de lógica de alto nivel para cumplir con tareas rutinarias. Ellos son la manera de encapsular código útil en POE. Las cosas comunes que se hacen con wheels en POE incluyen manejar entrada y salida conducida por eventos y crear conexiones de red de una manera fácil. A menudo se utilizan con Filters y Drivers para manejar y enviar los datos. Esta descripción es un tanto vaga, pero el código de más adelante provee ejemplos más concretos.

Componentes

Un componente es una sesión que está diseñada para ser controlada por otras sesiones. Las sesiones pueden enviar comandos al componente y recibir eventos de ellos, parecido a como un sistema operativo se comunica con sus procesos vía IPC. Algunos ejemplos de componentes son POE::Component::IRC, una interface para crear clientes IRC basados en POE, y POE::Component::Client::HTTP, un agente de usuario HTTP manejado por eventos. No usaremos Componentes en este tutorial, pero son una parte muy útil de POE.

Un ejemplo sencillo

En este ejemplo se hará un servidor demonio que acepta conexiones TCP e imprime la respuesta a un problema de aritmética simple que le envía un cliente. Cuando alguien se conecta al puerto 31008 mostrará en pantalla "Hola Cliente!". El cliente podrá enviar la expresión aritmética, terminada con un enter (por ejemplo '6 + 3 \n' o '50 / (7 - 2) \n', y el servidor retornará la respuesta. Sencillo, no?
Escribir tal programa en POE es tremendamente diferente al método tradicional de escribir demonios en Unix. Tendremos una sesión servidor escuchando por conexiones TCP entrantes en el puerto 31008. Cada vez que llegue una conexión, se creará una sesión hijo para manejar esa conexión. Cada sesión hijo interactuará con el usuario y en forma silenciosa muere cuando la conexión se cierra. La mejor parte es que solo toma 74 líneas de modular y simple Perl.
En forma inocente, el programa comienza así:

#!/usr/bin/perl -w
 use strict;
 use Socket qw(inet_ntoa);
 use POE qw( Wheel::SocketFactory  Wheel::ReadWrite
             Filter::Line          Driver::SysRW );
 use constant PORT => 31008;


Aquí importamos los módulos y las funciones que el programa usará. Definimos un valor constante para escuchar el puerto. Usamos qw() como un atajo para traer los módulos de POE que necesitamos en una sola línea. La manera equivalente de hacer esto mismo es:
use POE;
use POE::Wheel::SocketFactory;
use POE::Wheel:ReadWrite;
use POE::Filter::Line;
use POE::Driver::SysRW;

Ahora viene la parte buena:

POE::Session->create ( 
  inline_states => {
    _start => \&server_start,
    accept_new_client => \&accept_new_client,
    accept_failed => \&accept_failed,
    _stop  => \&server_stop,
  }
);
$poe_kernel->run();
exit;

Ese es el programa entero! Seteamos la sesión servidor principal, le decimos al kernel de POE que comience a procesar los eventos y entonces se hace un exit. El kernel considera que se acaba el programa cuando no quedan más sesiones que manejar, pero como pondremos un loop infinito dentro de la sesión, entonces nunca terminará de ejecutar. POE automáticamente exporta la variable $poe_kernel hacia el namespace del script cuando se ejecuta "use POE;".
La llamada a POE::Session->create necesita unas palabras de explicación. Cuando se crea la sesión se le da al kernel una lista de eventos que él aceptará. En el código de ejemplo le estamos diciendo que manejará los eventos start, accept_new_client, accept_failded y _stop llamando a las funciones server_start(), accept_new_client(), accept_failded() y server_stop() respectivamente. Cualquier otro evento que la sesión reciba será ignorado. Los eventos _start y _stop son especiales para POE: el estado _start es la primer cosa que ejecuta una sesión cuando es creada, y la sesión es pasada al estado _stop cuando el kernel está a punto de destruirla. Es decir, _start y _stop son básicamente un constructor y un destructor.
Ahora que hemos escrito el programa entero, tenemos que escribir código para los estados que nuestra sesión ejecutará mientras corre. Comenzaremos con server_start(), el cual se llama cuando la sesión servidor principal es creada al principio del programa:

sub server_start {
    $_[HEAP]->{listener} = new POE::Wheel::SocketFactory
      ( BindPort     => PORT,
        Reuse        => 'yes',
        SuccessEvent => 'accept_new_client',
        FailureEvent => 'accept_failed'
      );
    print "SERVER: Started listening on port ", PORT, ".\n";
}


Es un buen ejemplo de un estado en POE. Primero lo primero: notas la variable $_[HEAP]? POE tiene una manera especial de pasar argumentos. El array @_ es armado con montones de argumentos extra (una referencia al kernel actual y la sesión, el nombre del estado, una referencia a la heap y otras cosas). Para accederlas se indexa la variable @_ con varias constantes especiales que POE exporta, tales como HEAP, SESSION, KERNEL, STATE y ARG0 hasta ARG9 para acceder a los argumentos de estado del usuario. Como casi todas las decisiones de diseño de POE, el punto de este esquema es maximizar la compatibilidad hacia versiones anteriores sin perder en velocidad. El ejemplo de arriba almacena un wheel SocketFactory en la heap bajo la clave 'listener'.
El wheel POE::Wheel::SocketFactory es una de las cosas más sabrosas de POE. Se puede usar para crear cualquier tipo de socket (perdón, todavía no puede crear sockets UDP) sin preocuparse por los detalles. La declaración del ejemplo crea un SocketFactory que escucha sobre un puerto TCP específico (con la opción SO_REUSE seteada) para nuevas conexiones. Cuando una conexión es establecida éste llamará al estado accept_new_client() para pasar el nuevo cliente de socket; si algo va mal, llamará al estado accept_failed() para manejar el error. Eso es todo lo que hay de redes en POE!
Se almacena la wheel en la heap para salvarla de que accidentalmente sea recogida por el garbage-collector al finalizar el estado (de esta manera persiste entre todos los estados de la sesión). Ahora vamos por el estado server_stop():

sub server_stop {
    print "SERVER: Stopped.\n";
}


No hace mucho. Solamente ilustra el flujo del programa cuando corre. Podríamos no tener un estado _stop, pero es mucho más instructivo (y fácil de debuggear) de esta manera.
Ahora crearemos nuevas sesiones para las conexiones que llegan:


sub accept_new_client {
    my ($socket, $peeraddr, $peerport) = @_[ARG0 .. ARG2];
    $peeraddr = inet_ntoa($peeraddr);
    POE::Session->create(
      inline_states => {
        _start      => \&child_start,
        _stop       => \&child_stop,
        child_input => \&child_input,
        child_done  => \&child_done,
        child_error => \&child_error,
      },
      args => [ $socket, $peeraddr, $peerport ],
    );
    print "SERVER: Got connection from $peeraddr:$peerport.\n";
}


Nuestro POE::Wheel::SocketFactory llamará a esta subrutina si se establece una conexión satisfactoria con un cliente. La instrucción inet_ntoa se usa para convertir la dirección del socket en una dirección IP legible para humanos. Seteamos una nueva sesión para hablar con el cliente. Es similar al constructor anterior, pero tiene un par de cosas que explicaremos:
@_[ARG0 .. ARG2] es un atajo para ($_[ARG0],$_[ARG1],$_[ARG2]). Verás este tipo de slices de array muchas veces en los scripts POE.
Finalmente, la referencia al array al final de la lista de argumentos del constructor es la lista de argumentos que serán pasados manualmente al estado _start de la sesión.
Si hay problemas creando el socket de entrada o aceptando una conexión, el POE::Wheel::SocketFactory hará ésto:
sub accept_failed {
    my ($function, $error) = @_[ARG0, ARG2];
    delete $_[HEAP]->{listener};
    print "SERVER: call to $function() failed: $error.\n";
}


Imprimir un mensaje de error es lo normal, pero porqué borramos el wheel SocketFactory de la heap? La respuesta tiene que ver con la manera en que POE maneja los recursos sesión. Cada sesión es considerada "viva" mientras tenga manera de generar o recibir eventos. Si no tiene wheels o alias (una atractiva característica de POE que no veremos en este tutorial) el kernel POE se da cuenta que la sesión está muerta y el garbage-collector se la lleva. La única forma de que la sesión server pueda tener eventos es desde su wheel SocketFactory, si es destruída el kernel POE esperará hasta que todas sus sesiones hijas finalicen, entonces la sesión será recolectada. En ese momento no quedarán sesiones para ejecutar, entonces el kernel POE se queda sin trabajo y finaliza.
Así, básicamente, ésta es la manera normal de deshacerse de las sesiones POE no requeridas: se liberan los recursos de la sesión y se deja limpio el kernel. Ahora veremos detalles de las sesiones hijas:

sub child_start {
    my ($heap, $socket) = @_[HEAP, ARG0];
    $heap->{readwrite} = new POE::Wheel::ReadWrite
      ( Handle => $socket,
        Driver => new POE::Driver::SysRW (),
        Filter => new POE::Filter::Line (),
        InputEvent => 'child_input',
        ErrorEvent => 'child_error',
      );
    $heap->{readwrite}->put( "Hola cliente!" );
    $heap->{peername} = join ':', @_[ARG1, ARG2];
    print "CHILD: Connected to $heap->{peername}.\n";
}


Cada vez que una nueva sesión hija es creada se llama a child_start() para manejar una nueva conexión cliente. Se introduce un nuevo tipo de wheel aquí: el ReadWrite wheel, el cual maneja tareas de I/O. Se le pasa un filehandle, un driver que usará para llamadas de I/O, y un filtro que pasa los datos entrantes y salientes y los transforma (en este caso convierte un flujo crudo de datos del socket a lineas separadas, y vice versa). Entonces el wheel enviará a la sesión un evento child_input() cada vez que reciba datos del filehandle, y un evento child_error() si ocurre algún error.
De inmediato se usa el nuevo wheel para mandar el string "Hola cliente!" al socket (el filtro POE::Filter::Line agrega el terminador de línea por nosotros en el string). Finalmente, se almacena la dirección y puerto del cliente en la heap y se imprime un mensaje de éxito.
Ahora discutiremos el estado child_input():

sub child_input {
    my $data = $_[ARG0];
    $data =~ tr{0-9+*/()-}{}cd;
    return unless length $data;
    my $result = eval $data;
    chomp $@;
    $_[HEAP]->{readwrite}->put( $@ || $result );
    print "CHILD: Got input from peer: \"$data\" = $result.\n";
}


Cuando el cliente nos envía una linea de datos, la descomponemos en una simple expresión aritmética y la evaluamos, enviando el resultado o mensaje de error de regreso al cliente. Normalmente, pasar datos del usuario sin validar a la función eval() es una cosa horrible y peligrosa, entonces removemos cualquier carácter no aritmético antes de hacer la evaluación. La sesión hija aceptará cada nuevo dato del cliente hasta que éste cierre la conexión. Corre el código tú mismo para intentarlo!
El estado child_stop sólo tiene una línea de código. Los estados child_done() y child_error() son autoexplicativos, ellos borran el wheel ReadWrite de la sesión hija causando que sea colectada por el garbage-collector e imprimen una explicación de lo sucedido. Fácil.

sub child_stop {
  print "CHILD: Stopped.\n";
}
sub child_done {
  delete $_[HEAP]->{readwrite};
  print "CHILD: disconnected from ", $_[HEAP]->{peername}, ".\n";
}
sub child_error {
  my ($function, $error) = @_[ARG0, ARG2];
  delete $_[HEAP]->{readwrite};
  print "CHILD: call to $function() failed: $error.\n" if $error;
}


Eso es todo por hoy

Y eso es todo lo que hay. La subrutina más larga en el programa sólo tiene 12 líneas, y toda la parte complicada del servidor ha sido más llevadera gracias a POE. Pero podrías preguntarte si no sería más sencillo un estilo de programación procedural, como los ejemplos de "man perlipc". Para un programa como este simple ejemplo probablemente sí. Pero la belleza de POE es esa, a medida que tu programa escale, permanecerá fácil de modificar. Es más fácil organizar el script en elementos discretos, y POE provee todas las características que en otro caso hay que reinventar cada vez que se necesite.
Intenta usar POE en tu próximo proyecto. Cualquier cosa que necesite un event loop es un buen lugar para usar POE. A divertirse!

domingo, 7 de febrero de 2016

Manual POE::Session


El siguiente script crea una sesión de POE, luego arranca el Kernel. El kernel de POE pone en marcha la sesión, en este caso imprime tick cada un segundo y no termina nunca.

#!/usr/bin/perl -w
use POE; # auto-includes POE::Kernel and POE::Session

POE::Session->create(
  inline_states => {
     _start => sub { $_[KERNEL]->yield("next") },
     next   => sub {
       print "tick...\n";
       $_[KERNEL]->delay(next => 1);
     },
  },
);

POE::Kernel->run();
exit;


POE::Session es una tarea genérica orientada a eventos.
POE::Session y sus subclases traducen eventos del despachador genérico de POE::Kernel a llamadas convencionales al código de la aplicación. En términos de Desingn Patterns, las clases POE::Session son adaptadores que trabajan entre POE::Kernel y el código de la aplicación.
POE::Session tiene dos propósitos principales. Primero, mapea nombres de eventos con las piezas de código que los manejan. Segundo, mapea una consistente interface que despacha los eventos a sus manejadores.

martes, 2 de febrero de 2016

Manual POE


El siguiente script arranca 10 sesiones POE, cada una contando hasta 10. Cuando terminan de contar imprimen un mensaje de finalización y el script termina.

#!/usr/bin/perl -w
use strict;

use POE;  # Auto-includes POE::Kernel and POE::Session.

sub handler_start {
  my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
  print "Session ", $session->ID, " has started.\n";
  $heap->{count} = 0;
  $kernel->yield('increment');
}

sub handler_increment {
  my ($kernel, $heap, $session) = @_[KERNEL, HEAP, SESSION];
  print "Session ", $session->ID, " counted to ", ++$heap->{count}, ".\n";
  $kernel->yield('increment') if $heap->{count} < 10;
}

sub handler_stop {
  print "Session ", $_[SESSION]->ID, " has stopped.\n";
}

for (1..10) {
  POE::Session->create(
    inline_states => {
      _start    => \&handler_start,
      increment => \&handler_increment,
      _stop     => \&handler_stop,
    }
  );
}

POE::Kernel->run();
exit;


POE es un framework en Perl para programación multitarea y de redes cooperativa y orientada a eventos. Otros lenguajes tienen frameworks similares, como Python tiene Twisted y TCL tiene "the event loop".
POE provee una interface unificada para varios bucles de eventos, incluyendo select(), IO::Poll, Glib, Gtk, Tk, Wx, y Gtk2.
POE logra un alto grado de portabilidad gracias a ser enteramente escrito en Perl. Pero hay módulos opcionales en XS si la velocidad es más deseable que la portabilidad.
POE está diseñado en capas. Cada capa se construye sobre las demás. Los scripts son libres de usar POE en cualquier nivel de abstracción y los diferentes niveles pueden mezclarse. Sin embargo las abstracciones de alto nivel requieren más recursos que las de bajo nivel.

domingo, 31 de enero de 2016

Manejar archivos CSV con SQL

Como vimos anteriormente, los archivos CSV representan datos en forma de tabla. Las filas están separadas por saltos de línea y las columnas están separadas por comas o por punto y comas. Hoy vamos a usar el módulo DBI para acceder a los datos de estos archivos. El conjunto de herramientas DBI es una interfaz de acceso a distintas bases de datos, incluidas Oracle, Mysql y otros sistemas relacionales. La interfaz básica para consultar y actualizar la base de datos es el lenguaje SQL. El módulo DBI proporciona instrucciones SQL al módulo DBD::CSV, que a su vez pasa el control a otro módulo de interpretación de SQL.

Objetivo

Supongamos un sistema utilizado para registrar las consultas médicas. Una consulta médica tiene un archivo pacientes.csv con los datos personales de sus pacientes. Cada línea del archivo tiene el rut, el nombre y la edad de un paciente, separados por un símbolo ;. Así se ve el archivo:

12067539-7;Lorena López;32
15007265-4;Saúl Morales;26
8509454-8;Diego Muñoz;45
7752666-8;Gabriel Navarro;49
8015253-1;Darío Pacheco;51
9217890-0;Aldo Pimienta;39
9487280-4;Juan Rosas;42
12393241-2;Felipe Rubio;33
11426761-9;Samanta Pérez;35
15690109-1;José Ruiz;26
6092377-9;Alfonso Iúdica;65
9023365-3;Nancy Toledo;38
10985778-5;Tomás Valdés;38
13314970-8;Adán Vázquez;30
7295601-k;Wilson Muñoz;60
5106360-0;Alejandra Vega;71
8654231-5;Andrés Dib;55
10105321-0;Antonio Cabalgante;31
13087677-3;Walter Álvarez;28
9184011-1;Soledad Andrade;47
12028339-1;Jorge Bogado;29
10523653-0;Francisca Avaria;40
12187197-1;Felipe Mañas;36
5935556-2;Pablo Barriga;80
14350739-4;Eduardo Velo;29
6951420-0;Oscar Benítez;68
11370775-5;Hugo Leguizamón;31
11111756-k;Cristóbal Colón;34

Además, cada vez que alguien se atiende en la consulta, la visita es registrada en el archivo atenciones.csv, agregando una línea que tiene el rut del paciente, la fecha de la visita (en formato dia-mes-año) y el precio de la atención, también separados por ;. El archivo se ve así:

8015253-1;4-5-2016;69580
12393241-2;6-5-2016;57274
10985778-5;8-5-2016;73206
8015253-1;10-5-2016;30796
8015253-1;12-5-2016;47048
12028339-1;12-5-2016;47927
11426761-9;13-5-2016;39117
10985778-5;15-5-2016;86209
7752666-8;18-5-2016;41916
8015253-1;18-5-2016;74101
12187197-1;20-5-2016;38909
8654231-5;20-5-2016;75018
8654231-5;22-5-2016;64944
5106360-0;24-5-2016;53341
8015253-1;27-5-2016;76047
9217890-0;30-5-2016;57726
7752666-8;1-6-2016;54987
8509454-8;2-6-2016;76483
6092377-9;2-6-2016;62106
11370775-5;3-6-2016;67035
11370775-5;7-6-2016;47299
8509454-8;7-6-2016;73254
8509454-8;10-6-2016;82955
11111756-k;10-6-2016;56520
7752666-8;10-6-2016;40820
12028339-1;12-6-2016;79237
11111756-k;13-6-2016;69094
5935556-2;14-6-2016;73174
11111756-k;21-6-2016;70417
11426761-9;22-6-2016;80217
12067539-7;25-6-2016;31555
11370775-5;26-6-2016;75796
10523653-0;26-6-2016;34585
6951420-0;28-6-2016;45433
5106360-0;1-7-2016;48445
8654231-5;4-7-2016;76458

Note que las fechas están ordenadas de menos reciente a más reciente, ya que las nuevas líneas siempre se van agregando al final.

  • Escriba una función costo_total_paciente(rut) que entregue el costo total de las atenciones del paciente con el rut dado:

>>>Calcular el costo total del paciente 
Por favor, ingrese rut del paciente: 8015253-1
Costo total del paciente: 297572 

>>>Calcular el costo total del paciente 
Por favor, ingrese rut del paciente: 14350739-4
Costo total del paciente: 0


  • Escriba una función pacientes_dia(dia, mes, ano) que entregue una lista con los nombres de los pacientes que se atendieron el día señalado:

>>> Pacientes que se atendieron en una fecha dada
Por favor, ingrese el dia: 2
Por favor, ingrese el mes: 6
Por favor, ingrese el año: 2016
['Diego Muñoz', 'Alfonso Iúdica']
>>> Pacientes que se atendieron en una fecha dada
Por favor, ingrese el dia: 23
Por favor, ingrese el mes: 6
Por favor, ingrese el año: 2016
[]

  • Escriba una función pacientes_menores(edad) que construya un archivo CSV con los pacientes con edad <= a la edad dada.
Por ejemplo, el archivo jovenes.csv debe verse así:

>>>Pacientes menores a una edad dada
Por favor, ingrese la edad: 30
>>>jovenes.csv 
15007265-4;"Saúl Morales";26
15690109-1;"José Ruiz";26
13314970-8;"Adán Vázquez";30
13087677-3;"Walter Álvarez";28
12028339-1;"Jorge Bogado";29
14350739-4;"Eduardo Velo";29


Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;
use DBI;
use Data::Dumper;
 
# el costo total de las atenciones del paciente con el rut dado
sub costo_total_paciente{
  my $rut= shift; 
  # conectamos 
  my $dbh= DBI->connect('DBI:CSV:');
  my $tabla= 'atenciones.csv';
  # seteamos el atributo sep_char para que use el separador ; en vez de ,
  $dbh->{csv_sep_char}= ";";
  # describimos los nombres de las columnas del archivo atenciones.csv
  $dbh->{csv_tables}{$tabla} = {
        col_names => [qw( rut fecha costo )]
        };
  # ejecutamos la consulta como un comando SQL
  my $query= "SELECT sum(costo) as costo_total FROM $tabla WHERE rut='$rut'";
  my $sth  = $dbh->prepare($query);
  $sth->execute();
  my $row = $sth->fetchrow_hashref;
  my $costo_total= 0;
  if ($row->{costo_total}){ $costo_total= $row->{costo_total}; }
  $sth->finish();
  return $costo_total;
}

print "Calcular el costo total del paciente \n";
print "Por favor, ingrese rut del paciente: ";
my $rut = <stdin>;
chomp($rut);
my $costo_total= costo_total_paciente($rut);
print "Costo total del paciente: $costo_total \n";

# listar los nombres de los pacientes que se atendieron el día dado
sub pacientes_dia{
  my ($dia, $mes, $anio)= @_;
  # conectamos 
  my $dbh= DBI->connect('DBI:CSV:');
  # seteamos el atributo sep_char para que use el separador ; en vez de ,
  $dbh->{csv_sep_char}= ";";
  # describimos los nombres de las columnas del archivo atenciones.csv
  my $atenciones= 'atenciones.csv';
  $dbh->{csv_tables}{$atenciones} = {
        col_names => [qw( rut fecha costo )]
        };
  # describimos los nombres de las columnas del archivo pacientes.csv
  my $pacientes= 'pacientes.csv';
  $dbh->{csv_tables}{$pacientes} = {
        col_names => [qw( rut nombre edad )]
        };
  # ejecutamos la consulta como un comando SQL
  my $fecha= join('-',$dia,$mes,$anio);
  my $query= "SELECT distinct rut FROM $atenciones WHERE fecha like '$fecha%'";
  my $sth  = $dbh->prepare($query);
  $sth->execute();
  while ( my $row = $sth->fetchrow_hashref ) {
    $query= "SELECT nombre FROM $pacientes WHERE rut='$row->{rut}'";
    my $sth2= $dbh->prepare($query);
    $sth2->execute();
    my $row2= $sth2->fetchrow_hashref;
    print $row2->{nombre} . "\n";
    $sth2->finish();
  }
  $sth->finish();
  return;
}

print "Pacientes que se atendieron en una fecha dada\n";
print "Por favor, ingrese el dia: ";
my $dia = <stdin>;
chomp($dia);
print "Por favor, ingrese el mes: ";
my $mes = <stdin>;
chomp($mes);
print "Por favor, ingrese el año: ";
my $anio = <stdin>;
chomp($anio);
pacientes_dia($dia, $mes, $anio);


# generar un archivo jovenes.csv con los datos de los pacientes menores a cierta edad dada
sub pacientes_menores{
  my $edad= shift;
  # conectamos 
  my $dbh= DBI->connect('DBI:CSV:');
  # seteamos el atributo sep_char para que use el separador ; en vez de ,
  $dbh->{csv_sep_char}= ";";
  # describimos los nombres de las columnas del archivo atenciones.csv
  my $pacientes= 'pacientes.csv';
  $dbh->{csv_tables}{$pacientes} = {
        col_names => [qw( rut nombre edad )]
        };
  # creamos la nueva tabla de menores
  my $jovenes= 'jovenes.csv';
  $dbh->do("CREATE TABLE $jovenes (rut CHAR(12), nombre CHAR(50), edad CHAR(3))")
  || die "No se pudo crear la tabla " . $dbh->errstr();
  # ejecutamos la consulta como un comando SQL
  my $query= "SELECT * FROM $pacientes WHERE edad < '$edad'";
  my $sth  = $dbh->prepare($query);
  $sth->execute();
  while ( my $row = $sth->fetchrow_hashref ) {
    $dbh->do("INSERT INTO $jovenes VALUES (".
      $dbh->quote($row->{rut}) . "," .
      $dbh->quote($row->{nombre}) . "," .
      $dbh->quote($row->{edad}) . ")")
      || die "No puedo insertar un registro, " . $dbh->errstr();
  }
  $sth->finish();
  
  return;
}

print "Pacientes menores a una edad dada\n";
print "Por favor, ingrese la edad: ";
my $edad = <stdin>;
chomp($edad);
pacientes_menores($edad);

viernes, 15 de enero de 2016

Archivos de texto CSV en Perl

Los archivos CSV representan datos en forma de tabla. Las filas están separadas por saltos de línea y las columnas están separadas por comas o por punto y comas.

Objetivo

Supongamos un sistema utilizado para registrar las consultas médicas. Una consulta médica tiene un archivo pacientes.csv con los datos personales de sus pacientes. Cada línea del archivo tiene el rut, el nombre y la edad de un paciente, separados por un símbolo ;. Así se ve el archivo:

12067539-7;Lorena López;32
15007265-4;Saúl Morales;26
8509454-8;Diego Muñoz;45
7752666-8;Gabriel Navarro;49
8015253-1;Darío Pacheco;51
9217890-0;Aldo Pimienta;39
9487280-4;Juan Rosas;42
12393241-2;Felipe Rubio;33
11426761-9;Samanta Pérez;35
15690109-1;José Ruiz;26
6092377-9;Alfonso Iúdica;65
9023365-3;Nancy Toledo;38
10985778-5;Tomás Valdés;38
13314970-8;Adán Vázquez;30
7295601-k;Wilson Muñoz;60
5106360-0;Alejandra Vega;71
8654231-5;Andrés Dib;55
10105321-0;Antonio Cabalgante;31
13087677-3;Walter Álvarez;28
9184011-1;Soledad Andrade;47
12028339-1;Jorge Bogado;29
10523653-0;Francisca Avaria;40
12187197-1;Felipe Mañas;36
5935556-2;Pablo Barriga;80
14350739-4;Eduardo Velo;29
6951420-0;Oscar Benítez;68
11370775-5;Hugo Leguizamón;31
11111756-k;Cristóbal Colón;34

Además, cada vez que alguien se atiende en la consulta, la visita es registrada en el archivo atenciones.csv, agregando una línea que tiene el rut del paciente, la fecha de la visita (en formato dia-mes-año) y el precio de la atención, también separados por ;. El archivo se ve así:

8015253-1;4-5-2016;69580
12393241-2;6-5-2016;57274
10985778-5;8-5-2016;73206
8015253-1;10-5-2016;30796
8015253-1;12-5-2016;47048
12028339-1;12-5-2016;47927
11426761-9;13-5-2016;39117
10985778-5;15-5-2016;86209
7752666-8;18-5-2016;41916
8015253-1;18-5-2016;74101
12187197-1;20-5-2016;38909
8654231-5;20-5-2016;75018
8654231-5;22-5-2016;64944
5106360-0;24-5-2016;53341
8015253-1;27-5-2016;76047
9217890-0;30-5-2016;57726
7752666-8;1-6-2016;54987
8509454-8;2-6-2016;76483
6092377-9;2-6-2016;62106
11370775-5;3-6-2016;67035
11370775-5;7-6-2016;47299
8509454-8;7-6-2016;73254
8509454-8;10-6-2016;82955
11111756-k;10-6-2016;56520
7752666-8;10-6-2016;40820
12028339-1;12-6-2016;79237
11111756-k;13-6-2016;69094
5935556-2;14-6-2016;73174
11111756-k;21-6-2016;70417
11426761-9;22-6-2016;80217
12067539-7;25-6-2016;31555
11370775-5;26-6-2016;75796
10523653-0;26-6-2016;34585
6951420-0;28-6-2016;45433
5106360-0;1-7-2016;48445
8654231-5;4-7-2016;76458

Note que las fechas están ordenadas de menos reciente a más reciente, ya que las nuevas líneas siempre se van agregando al final.

  • Escriba una función costo_total_paciente(rut) que entregue el costo total de las atenciones del paciente con el rut dado:

>>>Calcular el costo total del paciente 
Por favor, ingrese rut del paciente: 8015253-1
Costo total del paciente: 297572 

>>>Calcular el costo total del paciente 
Por favor, ingrese rut del paciente: 14350739-4
Costo total del paciente: 0


  • Escriba una función pacientes_dia(dia, mes, ano) que entregue una lista con los nombres de los pacientes que se atendieron el día señalado:

>>> Pacientes que se atendieron en una fecha dada
Por favor, ingrese el dia: 2
Por favor, ingrese el mes: 6
Por favor, ingrese el año: 2016
['Diego Muñoz', 'Alfonso Iúdica']
>>> Pacientes que se atendieron en una fecha dada
Por favor, ingrese el dia: 23
Por favor, ingrese el mes: 6
Por favor, ingrese el año: 2016
[]

  • Escriba una función pacientes_menores(edad) que construya un archivo CSV con los pacientes con edad <= a la edad dada.
Por ejemplo, el archivo jovenes.csv debe verse así:

>>>Pacientes menores a una edad dada
Por favor, ingrese la edad: 30
>>>jovenes.csv 
15007265-4;"Saúl Morales";26
15690109-1;"José Ruiz";26
13314970-8;"Adán Vázquez";30
13087677-3;"Walter Álvarez";28
12028339-1;"Jorge Bogado";29
14350739-4;"Eduardo Velo";29


Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;
use Text::CSV;

print "Calcular el costo total del paciente \n";
print "Por favor, ingrese rut del paciente: ";
my $rut = <stdin>;
chomp($rut);
my $costo_total= costo_total_paciente($rut);
print "Costo total del paciente: $costo_total \n";

print "Pacientes que se atendieron en una fecha dada\n";
print "Por favor, ingrese el dia: ";
my $dia = <stdin>;
chomp($dia);
print "Por favor, ingrese el mes: ";
my $mes = <stdin>;
chomp($mes);
print "Por favor, ingrese el año: ";
my $anio = <stdin>;
chomp($anio);
pacientes_dia($dia, $mes, $anio);

print "Pacientes menores a una edad dada\n";
print "Por favor, ingrese la edad: ";
my $edad = <stdin>;
chomp($edad);
pacientes_menores($edad);

# el costo total de las atenciones del paciente con el rut dado
sub costo_total_paciente{
  my $rut= shift; 
  # seteamos el atributo sep_char para que use el separador ; en vez de ,
  # seteamos el atributo binary para acentos y eñes
  my $csv = Text::CSV->new ( { sep_char => ';', binary => 1 } )  
  or die "No puedo usar CSV: ".Text::CSV->error_diag ();

  my $costo_total= 0;
  open my $fh, "<:encoding(utf8)", "atenciones.csv" or die "atenciones.csv: $!";
  while ( my $row = $csv->getline( $fh ) ) {
    # si la primera columna concuerda con el rut dado, lo procesamos
    ($row->[0] eq $rut) or next; 
    $costo_total+= $row->[2];
  }
  $csv->eof or $csv->error_diag();
  close $fh;
  return $costo_total;
}

# listar los nombres de los pacientes que se atendieron el día dado
sub pacientes_dia{
  my ($dia, $mes, $anio)= @_;
  # seteamos el atributo sep_char para que use el separador ; en vez de ,
  # seteamos el atributo binary para acentos y eñes
  my $csv = Text::CSV->new ( { sep_char => ';', binary => 1 } )  
  or die "No puedo usar CSV: ".Text::CSV->error_diag ();

  my %pacientes;
  my $fecha= join('-',$dia,$mes,$anio);
  open my $fh, "<:encoding(utf8)", "atenciones.csv" or die "atenciones.csv: $!";
  while ( my $row = $csv->getline( $fh ) ) {
    # si la segunda columna concuerda con el dia dado, lo procesamos
    $row->[1] =~ m/^$fecha/ or next; 
    $pacientes{$row->[0]}= 1;
  }
  $csv->eof or $csv->error_diag();
  close $fh;
  listar_pacientes(keys %pacientes);
  return;
}
sub listar_pacientes{
  my (@pacientes)= @_;
  # seteamos el atributo sep_char para que use el separador ; en vez de ,
  # seteamos el atributo binary para acentos y eñes
  my $csv = Text::CSV->new ( { sep_char => ';', binary => 1 } )  
  or die "No puedo usar CSV: ".Text::CSV->error_diag ();

  open my $fh, "<:encoding(utf8)", "pacientes.csv" or die "pacientes.csv: $!";
  while ( my $row = $csv->getline( $fh ) ) {
    # si la primera columna concuerda con el paciente dado, lo procesamos
    $row->[0] or next;
    grep(/$row->[0]/,@pacientes) or next; 
    print $row->[1] . "\n";
  }
  $csv->eof or $csv->error_diag();
  close $fh;
  
  return;
}

# generar un archivo jovenes.csv con los datos de los pacientes menores a cierta edad dada
sub pacientes_menores{
  my $edad= shift;
  # seteamos el atributo sep_char para que use el separador ; en vez de ,
  # seteamos el atributo binary para acentos y eñes
  my $csv = Text::CSV->new ( { sep_char => ';', binary => 1 } )  
  or die "No puedo usar CSV: ".Text::CSV->error_diag ();

  my @menores;
  open my $fh, "<:encoding(utf8)", "pacientes.csv" or die "pacientes.csv: $!";
  while ( my $row = $csv->getline( $fh ) ) {
    # si la tercera columna es menor o igual a la edad dada, lo procesamos
    $row->[2] or next;
    ($row->[2]<=$edad) or next; 
    push(@menores,$row);
  }
  $csv->eof or $csv->error_diag();
  close $fh;
  
  $csv->eol ("\r\n");
  open $fh, ">:encoding(utf8)", "jovenes.csv" or die "jovenes.csv: $!";
  $csv->print ($fh, $_) for @menores;
  close $fh or die "jovenes.csv: $!";
  
  return;
}

viernes, 25 de diciembre de 2015

Serie de Fibonacci

Alejandro tiene un campo y su vecina, Selena, tiene una pareja de conejos. Selena quiere asociarse con Alejandro para la cría de conejos, le dice que en su campo pueden criarse juntos y multiplicarse. Le dice que al mes engendrarán una pareja de conejos y que esa pereja tardará un mes en engendrar otra pareja de conejos. A su vez la primer pareja seguirá engendrando parejas de conejos. Y así cada pareja de conejos seguirá teniendo parejas de conejos. Entonces Alejandro se pregunta: ¿Cuántos conejos tendrá al cabo de 12 meses? La respuesta está en la serie de Fibonacci.

Los números de Fibonacci Fk son una sucesión de números naturales definidos de la siguiente manera:
F0 = 0,
F1 = 1,
Fk = Fk−1 + Fk−2, cuando k≥2

En palabras simples, la sucesión de Fibonacci comienza con 0 y 1, y los siguientes términos siempre son la suma de los dos anteriores.



En la siguiente tabla, podemos ver los números de Fibonacci desde el 0-ésimo hasta el duodécimo.

n 0123456789101112...
Fn 01123581321345589144...


La sucesión puede utilizarse, de forma parecida, para contar el número de distintas rutas que puede seguir una abeja que va recorriendo las celdillas hexagonales de un panal.

Objetivo

Dado un número N obtener el número de Fibonacci n-ésimo.

La solución recursiva en Perl


La serie de Fibonacci puede definirse en forma recursiva, y es natural pensar una solución recursiva como eśta:

#!/usr/bin/perl
use strict;
use warnings;

print "Serie Fibonacci\n";
print "Por favor, ingrese n: ";
my $n = <stdin>;
chomp($n);
my $valor= fibonacci($n);
print "El valor F($n) es $valor \n";

sub fibonacci{
  my $n= shift;
  # F0 = 0,F1 = 1,
  if ($n<=1){ return $n; }
  return fibonacci($n-1) + fibonacci($n-2);
}

Pero si estudiamos lo que pasa con éstas llamadas recursivas vemos que se llaman múltiples veces con los mismos argumentos. Esto es ineficiente, además las múltiples llamadas son exponenciales, por ejemplo fibonacci(5) llama a fibonacci(4) y fibonacci(3); pero fibonacci(4) también llama a fibonacci(3) y a fibonacci(2); pero cada fibonacci(3) llama a fibonacci(2) y así hasta llegar al caso base.

La solución con memoización en Perl

La solución más obvia es ir guardando los resultados parciales en un array para poder utilizarlos luego. Cada resultado parcial es la respuesta a un subproblema del problema inicial.

#!/usr/bin/perl
use strict;
use warnings;

print "Serie Fibonacci\n";
print "Por favor, ingrese n: ";
my $n = <stdin>;
chomp($n);
my @serie;
$serie[0]= 0;
$serie[1]= 1;
my $valor= fibonacci($n);
print "El valor F($n) es $valor \n";

sub fibonacci{
  my $n= shift;
  if (defined($serie[$n])){ return $serie[$n]; }
  $serie[$n]= fibonacci($n-1) + fibonacci($n-2);
  return $serie[$n];
}


Uno podría pensar que la memoización es la manera más conveniente de resolver un problema de recursividad múltiple. Sin embargo, una función recursiva gasta un poco de memoria y tiempo en cada llamada recursiva. Por esta razón, la programación dinámica prescinde de la recursividad y resuelve el problema exclusivamente con la iteración.

La solución con programación dinámica en Perl

En general los pasos necesarios para resolver un problema con la programación dinámica son las siguientes:
  • Encontrar una definición recursiva para el problema (incluyendo los casos base)
  • Inicializar la estructura de memoria (normalmente un vector o una matriz)
  • Llenar el resultado para los casos base
  • Usar bucles para calcular el resultado para otros casos, empleando la definición recursiva (estrategia bottom-up)
La estrategia bottom-up consiste en resolver primero los subproblemas más pequeños, almacenar su solución, y luego resolver los problemas más complejos, usando los resultados almacenados.


La idea es llenar el vector o matriz de resultados de abajo arriba, empezando por los casos base. Al resolver un subproblema se aplica la misma idea recursiva para obtener el resultado. Sin embargo, si los subproblemas recursivos ya han sido resueltos no es necesario hacer ninguna llamada recursiva, sino que el resultado se puede calcular directamente a partir de los resultados guardados en memoria.

#!/usr/bin/perl
use strict;
use warnings;

print "Serie Fibonacci\n";
print "Por favor, ingrese n: ";
my $n = <stdin>;
chomp($n);
my $valor= fibonacci($n);
print "El valor F($n) es $valor \n";

sub fibonacci{
  my $n= shift;
  my @serie;
  $serie[0]= 0;
  $serie[1]= 1;
  for (my $i= 2; $i <= $n; $i++){
    $serie[$i] = $serie[$i-2] + $serie[$i-1];
  }
  return $serie[$n];
}



viernes, 18 de diciembre de 2015

Números primos

Primo o compuesto


Luis invitó a su primo Miguel a su casa. Mientras la mamá de Luis preparaba unas galletas caseras al horno para comer con la leche chocolatada, los primos revolvían unos libros de matemática y se encontraron con este teorema:

Para todo número primo p > 3, se tiene que p = 6k+1 ó p = 6k-1 

Luego de pensar un rato se dieron cuenta de que todos los números enteros pueden expresarse exactamente de un de las 6 posibles formas:
6k, 6k+1, 6k+2, 6k+3, 6k-2, ó 6k-1
-Claro! -dijo Luis- 6k es divisible por 6, por lo que no es primo.
-6k+2 es par, por lo que no es primo -agregó Miguel.
-6k-2 es par, así que tampoco es primo -se apuró a conjeturar Luis.
-Pero qué pasa con 6k+3?
-6k+3 es igual a 3(2k+1) que es divisible por 3, por lo que no es primo
-Por lo tanto, los números primos tienen que expresarse de la forma 6k+1 o 6k-1.
-Pero no todos los números de esa forma son primos, por ejemplo...
-Ya están las galletitas! A tomar la leche! -dijo la mamá.



Objetivo

Desarrolle un programa cuya entrada sea un entero positivo n, y cuya salida sea:

    primo, si el número es primo, y
    compuesto, si el número es compuesto.

Por ejemplo, si la entrada es 29, el programa debe decir primo. Si la entrada es 27, el programa debe decir compuesto.


Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

print "Por favor, ingrese un número mayor a 0: ";
my $n = <stdin>;
chomp($n);
if (primo($n)){ print "Primo!"; }
else{ print "Compuesto!"; }
print "\n";

sub primo{
  my $n= shift;
  if (1==$n){ return 0; }
  if (2==$n){ return 1; }
  if (3==$n){ return 1; }
  return teorema($n);
}
sub teorema{
  my $n= shift;
  my $resto= $n % 6;
  if (($resto != 1) && ($resto != 5)){ return 0; }
  my $raiz= sqrt($n);
  my $i= 1;
  while ((6*$i - 1) <= $raiz){
    if ( !($n % (6*$i + 1)) ){ return 0; }
    if ( !($n % (6*$i - 1)) ){ return 0; }
    $i++;
  }
  return 1;
}

Primeros m primos

Usando como base el programa diseñado en el ejercicio anterior, desarrolle otro programa que reciba como entrada un número entero positivo m y cuya salida sean los m primeros números primos.

Por ejemplo, si la entrada es 12, la salida del programa debe ser:

2
3
5
7
11
13
17
19
23
29
31
37

Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

print "Por favor, ingrese un número mayor a 0: ";
my $n = <stdin>;
chomp($n);
my $cantidad= 0;
my $i= 1;
while ($cantidad < $n){
  if (primo($i)){ print "$i\n"; $cantidad++; }
  $i++;
}
print "\n";

sub primo{
  my $n= shift;
  if (1==$n){ return 0; }
  if (2==$n){ return 1; }
  if (3==$n){ return 1; }
  return teorema($n);
}
sub teorema{
  my $n= shift;
  my $resto= $n % 6;
  if (($resto != 1) && ($resto != 5)){ return 0; }
  my $raiz= sqrt($n);
  my $i= 1;
  while ((6*$i - 1) <= $raiz){
    if ( !($n % (6*$i + 1)) ){ return 0; }
    if ( !($n % (6*$i - 1)) ){ return 0; }
    $i++;
  }
  return 1;
}

Primos hasta m


Modifique el programa del ejercicio anterior para que muestre los números primos menores o iguales a m.

Por ejemplo, si la entrada es 12, la salida debe ser:

2
3
5
7
11

Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

print "Por favor, ingrese un número mayor a 0: ";
my $n = <stdin>;
chomp($n);
my $i= 1;
while ($i <= $n){
  if (primo($i)){ print "$i\n"; }
  $i++;
}
print "\n";

sub primo{
  my $n= shift;
  if (1==$n){ return 0; }
  if (2==$n){ return 1; }
  if (3==$n){ return 1; }
  return teorema($n);
}
sub teorema{
  my $n= shift;
  my $resto= $n % 6;
  if (($resto != 1) && ($resto != 5)){ return 0; }
  my $raiz= sqrt($n);
  my $i= 1;
  while ((6*$i - 1) <= $raiz){
    if ( !($n % (6*$i + 1)) ){ return 0; }
    if ( !($n % (6*$i - 1)) ){ return 0; }
    $i++;
  }
  return 1;
}

 Más información

Un ejemplo de algoritmo eficiente y su explicación se puede encontrar en la Criba de Atkin

miércoles, 16 de diciembre de 2015

PI

Un poco de historia

El valor de π se ha obtenido con diversas aproximaciones a lo largo de la historia, siendo una de las constantes matemáticas que más aparece en las ecuaciones de la física, junto con el número e. Por ello, tal vez sea la constante que más pasiones desata entre los matemáticos profesionales y aficionados.
Tomando en cuenta que el número pi forma un decimal infinito, lo habitual es usar una aproximación del mismo.

Los matemáticos han encontrado varias series matemáticas que si se repiten infinitamente pueden calcular con precisión el valor de Pi con una gran cantidad de decimales. Algunas de estas series son tan complejas que se necesitan supercomputadoras para procesarlas. Sin embargo, una de las más simples, es la serie Gregory-Leibniz. Aunque no es muy eficiente, se acerca cada vez más al valor de Pi en cada repetición, produciendo con precisión hasta cinco mil decimales de Pi con 500000 repeticiones.

Objetivo I

Desarolle un programa para estimar el valor de π usando la siguiente suma infinita:
π = 4*(1 − 1/3 + 1/5 − 1/7 + ···)

La entrada del programa debe ser un número entero n que indique cuántos términos de la suma se utilizará.

Por ejemplo, si la entrada es 3, el programa debe entregar como salida:

3.466666666666667

Si la entrada es 1000, la salida debe ser:

3.140592653839794

Mientras más veces repitas la serie, más te acercarás al valor de Pi.


Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

my $suma= 0;
print "Serie Gregory-Leibniz\n";
print "Por favor, ingrese la cantidad de terminos: ";
my $n = <stdin>;
chomp($n);
my $fin_iteracion= $n-1;
foreach my $i (0..$fin_iteracion){
  my $sumando= 1/($i*2+1);
  if ($i%2){ $suma-= $sumando; }
  else{ $suma+= $sumando; }
}
my $aproximacion= 4 * $suma;
print "El valor aproximado de Pi es $aproximacion \n";



Objetivo II

Utilice la serie Nilakantha. Esta es otra serie infinita que sirve para calcular Pi y que además es bastante fácil de entender. Aunque es más complicada que la fórmula de Gregory-Leibniz, converge en los valores de Pi mucho más rápido.

π = 3 + 4/(2*3*4) - 4/(4*5*6) + 4/(6*7*8) - 4/(8*9*10) + 4/(10*11*12) - 4/(12*13*14) ...
Para esta fórmula, toma un tres y empieza a alternar entre suma y resta de fracciones con un numerador de 4 y un denominador que sea el producto de tres enteros consecutivos que vayan aumentando con cada nueva fracción. El denominador de cada nueva fracción empieza con el mayor entero utilizado en la fracción anterior. Repite la serie aunque sea solo un par de veces y verás que el resultado se acerca bastante a Pi.



Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

my $suma= 0;
print "Serie Nilakantha\n";
print "Por favor, ingrese la cantidad de terminos: ";
my $n = <stdin>;
chomp($n);
foreach my $i (1..$n){
  my $denominador= ($i*2)*($i*2+1)*($i*2+2);
  my $sumando= 4/$denominador;
  if ($i%2){ $suma+= $sumando; }
  else{ $suma-= $sumando; }
}
my $aproximacion= 3 + $suma;
print "El valor aproximado de Pi es $aproximacion \n";

lunes, 14 de diciembre de 2015

Paradoja de la dicotomía

Problema

Zenón está a ocho metros de un árbol. Llegado un momento, lanza una piedra, tratando de dar al árbol. La piedra, para llegar al objetivo, tiene que recorrer antes la primera mitad de la distancia que le separa de él, es decir, los primeros cuatro metros, y tardará un tiempo (finito) en hacerlo. Una vez llegue a estar a cuatro metros del árbol, deberá recorrer los cuatro metros que le quedan, y para ello debe recorrer primero la mitad de esa distancia. Pero cuando esté a dos metros del árbol, tardará tiempo en recorrer el primer metro, y luego el primer medio metro restante, y luego el primer cuarto de metro... De este modo, la piedra nunca llegará al árbol. Es posible utilizar este razonamiento, de forma análoga, para «demostrar» que la piedra nunca llegará a salir de la mano de Zenón.

Potencias fraccionales de 2


Desarrolle un programa que tabule las potencias fraccionales de 2 (1/2, 1/4, 1/8, 1/16, ...) y sus sumas parciales en forma decimal.

La salida del programa debe comenzar así:

Potencia  Fraccion  Suma
1          0.5       0.5
2          0.25      0.75
3          0.125     0.875
4          0.0625    0.9375
...       ...

El programa debe terminar cuando la fracción sea menor o igual que 0.00001.

La tercera columna contiene la suma de todas las fracciones calculadas hasta esa fila.

Solución en Perl

Usamos printf para imprimir las fracciones con una cantidad específica de decimales por un motivo estético

#!/usr/bin/perl
use strict;
use warnings;

my $n= 1;
my $fraccion= 1/(2**$n);
my $suma= $fraccion;
print "Potencia \t\tFraccion \t\tSuma\n";
while ($fraccion>0.00001){
  print "$n \t\t";
  printf "%.16f \t\t%.5f \n",$fraccion,$suma;
  $n++;
  $fraccion= 1/(2**$n);
  $suma+= $fraccion;
}

martes, 8 de diciembre de 2015

Largo y corto

100 Nombres

Daniel y Cristina van a tener un bebé. Todavía no saben si será nene o nena pero tienen una lista con los posibles nombres. Como les cuesta decidirse por alguno quieren divertirse diseñando un programa de computadora que les diga el nombre más corto y el más largo.

Objetivo

Desarrolle un programa que tenga la siguiente entrada:
  • primero, el usuario ingresa un número entero n, que indica cuántas palabras ingresará a continuación;
  • después el usuario ingresa n palabras.

La salida del programa deben ser la palabra más larga y la más corta que ingresó el usuario.

Por ejemplo, si el usuario ingresa:

5
Diego
Andrea
Marcelino
Azul
Ana

la salida debe ser:

Mas larga: Marcelino
Mas corta: Ana

Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

my $mas_corto= '';
my $mas_largo= '';
my $size_corto= 99;
my $size_largo= 0;
print "Por favor, ingrese la cantidad de nombres: ";
my $n = <stdin>;
chomp($n);
foreach my $i (1..$n){
  print "Nombre $i?: ";
  my $nombre = <stdin>;
  chomp($nombre);
  if (length($nombre) > $size_largo){ $mas_largo= $nombre; $size_largo= length($nombre); }
  if (length($nombre) < $size_corto){ $mas_corto= $nombre; $size_corto= length($nombre); }
}
print "Gracias por los valores ingresados \n";
print "El más corto: $mas_corto \n";
print "El más largo: $mas_largo \n";


Contador de signos

Signos del clima

Ramón está preocupado por el cambio climático y decide empezar a revisar cifras que tengan que ver con el clima y su evolución. Para empezar toma un listado de temperaturas alrededor del mundo y quiere contar las que están sobre cero y las que están bajo cero. Como el trabajo es muy repetitivo y propenso a errores se propone automatizarlo con un programa de computadora.

Objetivo

Desarrolle un programa que pida al usuario que ingrese varios números, uno por uno. El ingreso de números termina cuando el usuario ingresa el 0, entonces el programa debe entregar como salida la cantidad de números positivos y negativos que ingresó el usuario.

Por ejemplo, si el usuario ingresa los números 37, —4, —22, —7, 17, —1 y 0, el programa debe entregar la salida:

Positivos: 2
Negativos: 4


Solución en Perl

#!/usr/bin/perl
use strict;
use warnings;

my $positivos= 0;
my $negativos= 0;
print "Por favor, ingrese un valor: ";
my $temperatura = <stdin>;
chomp($temperatura);
while($temperatura){
  if ($temperatura > 0){ $positivos++; }
  if ($temperatura < 0){ $negativos++; }
  print "Por favor, ingrese un valor: ";
  $temperatura = <stdin>;
  chomp($temperatura);
}
print "Gracias por los valores ingresados \n";
print "Positivos: $positivos \n";
print "Negativos: $negativos \n";

lunes, 30 de noviembre de 2015

Tablas de multiplicar

Pamela aprende a multiplicar

Pamela llega de la escuela contenta porque aprendió que la multiplicación le ahorra tiempo cuando tiene que sumar varias veces el mismo número. Se lo explica a su hermano mayor, David, y le pregunta dónde puede encontrar las tablas de multiplicar. David lo piensa dos veces y se le ocurre que es una buena oportunidad para llamar a su primo Vladimir. Vladimir llega con su netbook y su lenguaje de programación favorito y juntos planean cómo imprimir las tablas de multiplicar.

Objetivo

Desarrolle un programa que imprima la tabla de multiplicar del número elegido.
Por ejemplo, si se elige el número 9, la salida del programa debe ser:

9 x 1 = 9
9 x 2 = 18
9 x 3 = 27
9 x 4 = 36
9 x 5 = 45
9 x 6 = 54
9 x 7 = 63
9 x 8 = 72
9 x 9 = 81
9 x 10 = 90

Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

print "Ingresar el factor: ";
my $factor = <stdin>;
chomp($factor);
if (! $factor){ die; }

foreach my $i (1..10){
  my $producto= $i*$factor;
  print "$factor x $i = $producto \n";
}

lunes, 23 de noviembre de 2015

Resolver una ecuación cuadrática

Ecuación cuadrática


Desarrolle un programa que resuelva la ecuación cuadrática ax2+bx+c=0.

La entrada del programa serán a, b y c. La salida serán las soluciones reales de la ecuación.

Por ejemplo, si la entrada son los valores 1, 2 y —8, la salida del programa debe ser:

-4.0
2.0

Si la entrada son los valores 4, 4 y 1, la salida del programa debe ser:

-0.5

Si la entrada son los valores 3, —3 y 1, la salida del programa debe ser:

no hay soluciones reales

Solución


Resolveremos la ecuación reemplazando a, b y c en la fórmula cuadrática:
x= (-b ± √(b2- 4ac) )/ 2a

El discriminante de la fórmula determina cuántas soluciones reales hay, es decir la cantidad de valores de x que satisfacen la ecuación. Si el discriminante es cero hay una solución, si es mayor a 0 hay dos soluciones y si es negativo no hay soluciones reales. El discriminante se calcula como Δ= b2 - 4ac

Cuando el discriminante es cero, la fórmula cuadrática  que queda es x= -b/2a
Cuando el discriminante es mayor a cero nos quedan dos soluciones:
x= (-b + √Δ) / 2a
x= (-b - √Δ) / 2a

Solución en Javascript


function cuadratica(){
  var a;
  var b;
  var c;

  a= prompt( 'Ingresar el valor a: ', 1 );
  // si a es cero o vacio no es una función cuadrática
  if (! (a*1)) return;

  b= prompt( 'Ingresar el valor b: ', 1 );
  c= prompt( 'Ingresar el valor c: ', 1 );

  var discriminante= b*b - 4*a*c;
  if (discriminante < 0){ 
    alert( 'No hay soluciones reales' );
    return;
  }

  if (discriminante){ 
    alert( 'Hay dos soluciones:' ); 
    var solucion = (-b + Math.sqrt(discriminante))/(2*a);
    alert( solucion ); 
    var solucion2 = (-b - Math.sqrt(discriminante))/(2*a);
    alert( solucion2 ); 
  }else{
    var solucion = -b/(2*a);
    alert( 'La unica solucion es ' + solucion );
  }
  return;
}
cuadratica();
cuadratica();
cuadratica();

Solución en Perl


#!/usr/bin/perl
use strict;
use warnings;

print "Ingresar el valor a: ";
my $a = <stdin>;
chomp($a);
if (! $a){ die; }

print "Ingresar el valor b: ";
my $b = <stdin>;
chomp($b);

print "Ingresar el valor c: ";
my $c = <stdin>;
chomp($c);

my $discriminante= $b*$b - 4*$a*$c;
if ($discriminante < 0){ 
  print "No hay soluciones reales \n";
  exit;
}

if ($discriminante){ 
  print "Hay dos soluciones: \n"; 
  my $solucion = (-$b + sqrt($discriminante))/(2*$a);
  print $solucion."\n"; 
  my $solucion2 = (-$b - sqrt($discriminante))/(2*$a);
  print $solucion2."\n"; 
}else{
  my $solucion = -$b/(2*$a);
  print "La unica solucion es $solucion \n";
}


jueves, 19 de noviembre de 2015

Número recíproco


Recíproco o Inverso multiplicativo


El recíproco de un número real x se calcula como 1 dividido por x, excepto cuando x=0, ya que el cero no tiene recíproco.

Desarolle un programa que reciba como entrada un número real, y que entregue como salida el recíproco del número. Si la entrada es 0, la salida debe decir: el cero no tiene reciproco.

Solución en Perl

#!/usr/bin/perl
use strict;
use warnings;

print "Ingresar número: ";
my $valor = <stdin>;
chomp($valor);

if (! $valor){ print "El cero no tiene reciproco \n"; }
else{
  my $reciproco = 1/$valor;
  print "El reciproco de $valor es $reciproco \n";
}

Solución en Javascript

var valor= prompt('Ingresar número',0);
if (! valor){ alert('El cero no tiene reciproco'); }
else{
  var reciproco;
  reciproco = 1/valor;
  alert ('El reciproco de ' + valor + ' es ' + reciproco);
}