Práctica: Números Fraccionarios

Construya un módulo que permita el uso de números fraccionarios como $ \frac{4}{5}, \frac{2}{9}, \ldots$, etc. y las operaciones habituales entre los mismos.

El Constructor: Requerimientos de Polimorfismo

Se desea que el contructor sea lo suficientemente polimorfo para admitir las siguientes formas de llamada:

lhp@nereida:~/Lperl/src$ perl -MNumber::Fraction -de 0
  DB<1> x Number::Fraction->new(2,4)
0  Number::Fraction=HASH(0x84666a0)
   'den' => 2
   'num' => 1
  DB<2> x Number::Fraction->new(2) # Un número. El denominador es 1
0  Number::Fraction=HASH(0x8527660)
   'den' => 1
   'num' => 2
  DB<3> x Number::Fraction->new(2,0) # Error. El denominador es cero.
Can't make a Number::Fraction with demominator 0 
  DB<4> $b = Number::Fraction->new # Sin número: es 0/1
  DB<5> x $b->new # LLamado por un objeto. Se copia el objeto
0  Number::Fraction=HASH(0x85281f4)
   'den' => 1
   'num' => 0
  DB<6> $b = Number::Fraction->new(2,5) # Dos números: numerador y denominador
  DB<7> x Number::Fraction->new($b) # Un argumento objeto: se copia
0  Number::Fraction=HASH(0x8528410)
   'den' => 5
   'num' => 2

El Constructor: Solución

Para simplificar la práctica aquí esta un constructor que cumple los requerimientos:

lhp@nereida:~/Lperl/src$ sed -ne '31,74p' Number/Fraction.pm | cat -n
 1  {
 2    my $isint = qr{^\s*-?\d+\s*$};
 3    my $isfrac =  qr{^\s*(-?\d+)(/(-?\d+))?$};
 4
 5    sub new {
 6      my $class = shift;
 7
 8      my $self;
 9      if (@_ >= 2) {
10        return unless $_[0] =~ $isint and $_[1] =~ $isint;
11
12        $self->{num} = $_[0];
13        unless ($_[1]) {
14          carp "Can't make a $class with demominator 0";
15          return;
16        }
17        $self->{den} = $_[1];
18      }
19      elsif (@_ == 1) {
20        if (ref $_[0]) { # Number::Fraction->new($x) y $x objeto. Lo copiamos
21          return $class->new($_[0]->{num}, $_[0]->{den}) if (UNIVERSAL::isa($_[0], __PACKAGE__));
22          croak "Can't make a $class from a ", ref $_[0];
23        } else { # Es una cadena 'num/num' o 'num'
24          return unless $_[0] =~ $isfrac;
25
26          $self->{num} = $1;
27          $self->{den} = $3 || 1;
28        }
29      }
30      elsif (ref($class)) { # LLamado $x->new. Copiamos el objeto
31        %$self = %$class;
32      }
33      else {
34        $self->{num} = 0;
35        $self->{den} = 1;
36      }
37
38      bless $self, ref($class) || $class;
39
40      $self->normalise;
41
42      return $self;
43    }
44  }

La Función de Normalización de una Fracción

A continuación sigue el código de normalise. La función ''normaliza'' un quebrado, esto es lo reduce a una fracción irreducible.

sub normalise {
  my $self = shift;

  # Calculamos el máximo común divisor
  my $hcf = _hcf($self->{num}, $self->{den});

  # ... y dividmos ambos por el hcf
  $self->{num} /= $hcf;
  $self->{den} /= $hcf;

  # Denominador positivo en forma normalizada
  if ($self->{den} < 0) {
    $self->{num} *= -1;
    $self->{den} *= -1;
  }
}

Cálculo del Máximo Común Divisor

La subrutina normalise llama a _hcf (del inglés highest common factor o en español máximo común divisor) para permitir la reducción de la fracción:

sub _hcf {
  my ($x, $y) = @_;
  ($x, $y) = ($y, $x) if $y > $x;
  return $x if $x == $y;
  while ($y) {
    ($x, $y) = ($y, $x % $y);
  }
  return $x;
}

La subrutina _hcf es ''puro cálculo'' por lo que la implementación Perl es obviamente inferior en rendimiento a la correspondiente versión C:

$ cat -n hcf.c
 1  int hcf(int x, int y) {
 2    int t;
 3
 4    if (y > x) {
 5      t = x; x = y; y = t;
 6    }
 7    if (x == y) return x;
 8    while (y) {
 9      t = x;
10      x = y;
11      y = t % y;
12    }
13    return x;
14  }

El Módulo P5NCI

Para poder llamar a la versión C de la subrutina desde Perl usando el módulo P5NCI lo único que necesitamos es crear una librería dinámica:

$ cc  -shared  hcf.o -o libhcf.so
$ ls -ltr | tail -1
-rwxr-xr-x  1 lhp lhp 5446 2007-05-29 16:52 libhcf.so
$ nm libhcf.so                  # nm nos lista los símbolos en la librería 
00001650 A __bss_start
000003c0 t call_gmon_start
00001650 b completed.4463
00001558 d __CTOR_END__
00001554 d __CTOR_LIST__
         w __cxa_finalize@@GLIBC_2.1.3
000004f0 t __do_global_ctors_aux
000003f0 t __do_global_dtors_aux
00001648 d __dso_handle
00001560 d __DTOR_END__
0000155c d __DTOR_LIST__
00001568 a _DYNAMIC
00001650 A _edata
00001654 A _end
00000534 T _fini
00000450 t frame_dummy
00000550 r __FRAME_END__
00001634 a _GLOBAL_OFFSET_TABLE_
         w __gmon_start__
0000048c T hcf                    # Nuestra función máximo común divisor
00000485 t __i686.get_pc_thunk.bx
0000036c T _init
00001564 d __JCR_END__
00001564 d __JCR_LIST__
         w _Jv_RegisterClasses
0000164c d p.4462

LLamada desde Perl

El módulo P5NCI permite cargar la librería desde Perl y llamar a las funciones:

$ cat -n usehcf.pl
 1  #!/usr/local/bin/perl -w
 2  use strict;
 3  use P5NCI::Library;
 4
 5  my $lib = P5NCI::Library->new( library => './libhcf.so' );
 6  $lib->install_function( 'hcf', 'iii' );
 7
 8  print hcf( 20, 10 ), "\n";
 9  print hcf( 12, 9 ), "\n";
10  print hcf( 18, 12 ), "\n";
La llamada a P5NCI::Library->new carga la librería. La subsiguiente llamada al método install busca la función en la librería y crea una interfaz Perl para la especificación dada por la firma 'iii'. Esta firma indica que la función recibe dos enteros y devuelve un entero. Observe que mediante P5NCI es posible usar funciones cuyo fuente no esta disponible. Tampoco importa en que lenguaje esté escrito. Importa que dispongamos de la librería y que conozcamos el nombre y la interfaz de la función.

El constructor admite la opción path que permite especificar el path de búsqueda para la librería. En este caso podemos usar el ''nombre'' oficial de la librería (hcf) y no la especifciación completa. También dispone de una opción package que permite especificar el paquete en el que se aloja la función.

$ cat -n usehcf2.pl
 1  #!/usr/local/bin/perl -w
 2  use strict;
 3  use P5NCI::Library;
 4
 5  my $lib = P5NCI::Library->new( library => 'hcf', path => '.' );
 6  print "Path de búsqueda:\n@DynaLoader::dl_library_path\n";
 7  $lib->install_function( 'hcf', 'iii' );
 8
 9  print "hcf( 20, 10 ) = ",hcf( 20, 10 ), "\n";
10  print "hcf( 12,  9 ) = ",hcf( 12, 9 ), "\n";
11  print "hcf( 18, 12 ) = ",hcf( 18, 12 ), "\n";

La variable DynaLoader::dl_library_path contiene el camino de búsqueda de las librerías dinámicas.

Ejecución

Ahora al ejecutar el programa obtenemos el máximo común divisor para cada una de las tres parejas:

$ usehcf2.pl
Path de búsqueda:
. /usr/local/lib /lib /usr/lib
hcf( 20, 10 ) = 10
hcf( 12,  9 ) = 3
hcf( 18, 12 ) = 6


La portabilidad de P5NCI

La portabilidad de P5NCI es todavía insuficiente. El porcentaje de plataformas en las que funciona es bajo aún. Existen otros mecanismos para empotrar otros lenguajes en Perl: XS, Inline, etc. que son mas robustos.

El Módulo Inline

El módulo Inline proporciona los medios para realizar las pasarelas entre el lenguaje Perl y otro lenguaje de programación. En particular Inline::C facilita la integración entre código C y código Perl. Veamos un ejemplo:

$ cat -n hcfinline.pl
 1  #!/usr/local/bin/perl -w
 2  use strict;
 3  use Inline 'C';
 4
 5  print hcf( 20, 10 ), "\n";
 6  print hcf( 12, 9 ), "\n";
 7  print hcf( 18, 12 ), "\n";
 8
 9  __END__
10  __C__
11
12  int hcf(int x, int y) {
13    int t;
14
15    if (y > x) {
16      t = x; x = y; y = t;
17    }
18    if (x == y) return x;
19    while (y) {
20      t = x;
21      x = y;
22      y = t % y;
23    }
24    return x;
25  }
El código C puede almacenarse dentro del fichero DATA (esto es, a partir de la aparición del marcador __END__ en una sección determinada por el marcador __C__. También puede guardarse en una cadena ordinaria que es pasada a Inline::C:
$ cat -n shcfinline.pl
 1  #!/usr/local/bin/perl -w
 2  use strict;
 3  use Inline 'C' => q{
 4    int hcf(int x, int y) {
 5      int t;
 6
 7      if (y > x) {
 8        t = x; x = y; y = t;
 9      }
10      if (x == y) return x;
11      while (y) {
12        t = x;
13        x = y;
14        y = t % y;
15      }
16      return x;
17    }
18  };
19
20  print hcf( 20, 10 ), "\n";
21  print hcf( 12, 9 ), "\n";
22  print hcf( 18, 12 ), "\n";
Esta segunda forma es preferible a la hora de realizar la práctica.

Ejecutémos dos veces el ejemplo y cronometremos los tiempos de ejecución:

$ time shcfinline.pl
10
3
6

real    0m1.797s
user    0m1.520s
sys     0m0.250s
$ time shcfinline.pl
10
3
6

real    0m0.065s
user    0m0.060s
sys     0m0.010s
Vemos que la segunda vez tarda menos que la primera. Esto es así porque la primera vez Inline genera una librería dinámica que - por defecto - se guarda en el subdirectorio __Inline:
$ ls -ltr | tail -1
drwxr-xr-x  4 lhp lhp 4096 2007-05-31 07:42 _Inline
$ tree _Inline/
_Inline/
|-- build
|-- config
`-- lib
    `-- auto
        `-- shcfinline_pl_677d
            |-- shcfinline_pl_677d.bs
            |-- shcfinline_pl_677d.inl
            `-- shcfinline_pl_677d.so

4 directories, 4 files
Además genera el código de traducción de las llamadas desde Perl a las funciones C.

Durante las ejecuciones posteriores Inline comprueba que el código fuente no ha cambiado. Si es así carga la librería dinámica generada.

Cabeceras Reconocidas por Inline::C

La gramática de Inline para C reconoce ciertas definiciones de funciones del código C. Esto es, Inline generará el código necesario para enlazar la llamada a la función como si fuera una subrutina Perl. Si no puede reconocer la definición, esta será ignorada sin que hayan mensajes de error o advertencia. No quedará disponible en el ámbito de Perl, pero podrá aún seguir siendo usada en el ámbito de C. Inline busca por definiciones de funciones de estilo:

tipo_de_retorno nombre_de_funcion ( pares_tipo_identificador ) { cuerpo }

en las parejas tipo identificador, ... puede usarse cualquier tipo que esté definido en el fichero typemap.

Las siguientes definiciones no serán reconocidas:

Foo(int i) # no hay tipo de retorno
int foo(float f) { # no existe definición en typemap para float
int Foo(num) double num; { # la vieja sintáxis C no se soporta
void Foo(void) { # void se permite solo para el retorno

Inline y Otros Lenguajes

Inline no está limitado a C y puede ser usado con otros lenguajes. El siguiente ejemplo muestra como usar Inline::Python para usar código Python desde C:

lhp@nereida:~/Lperl/src/testing$ cat -n python.pl
 1  #!/usr/bin/perl -w
 2  use strict;
 3
 4  use Inline Python => <<'END_OF_PYTHON_CODE';
 5  def add(x,y):
 6    return x + y
 7
 8  def subtract(x,y):
 9    return x - y
10
11  END_OF_PYTHON_CODE
12
13  print "9 + 16 = ", add(9, 16), "\n";
14  print "9 - 16 = ", subtract(9, 16), "\n";
Al ejecutar este programa obtenemos la siguiente salida:
lhp@nereida:~/Lperl/src/testing$ python.pl
9 + 16 = 25
9 - 16 = -7

Reescriba en C las Funciones de Cálculo

Cuando escriba esta práctica haga uso de P5NCI o de Inline para la construcción de las funciones numéricas con prototipos simples que aparezcan durante la construcción del módulo. Basta con que considere la función del cálculo del máximo común divisor. Si se siente realmente fuerte y capacitado puede leer la sección como retornar múltiples valores del Inline::C Cookbok para ver como implantar la suma y el producto de fracciones.

Este es un ejemplo de como hacerlo: Se deben empujar - usando Inline_Stack_Push - en la pila del intérprete Perl los valores de retorno:

void add_c(int n1, int d1, int n2, int d2) {
  n1 = n1*d2 + n2*d1;
  d1 = d1*d2;
  Inline_Stack_Vars;
  Inline_Stack_Reset;
  Inline_Stack_Push(sv_2mortal(newSViv(n1)));
  Inline_Stack_Push(sv_2mortal(newSViv(d1)));
}
La función Inline_Stack_Vars define un conjunto de variables internas que son necesarias para poder acceder a la pila del intérprete Perl.

En general, cuando la función sea de tipo void como es el caso del ejemplo, se deben usar las macros con nombre Inline_Stack_. También si la función tiene un número variable de argumentos (uso de la elípsis ...).

Antes de comenzar a empujar items en la pila se debe llamar a la función Inline_Stack_Reset. Inicializa el puntero de pila interno al comienzo de la pila.

A partir del númerador n1 = n1*d2 + n2*d1 debemos construir el escalar Perl que guarda el número. La función newSViv es la que lo permite. La notación newSViv significa new Scalar Value from an Integer Value. El valor retornado por esta función es la estructura de datos que representa un valor escalar Perl, conocido en la jerga como SV (véase perlguts). Existe una familia de constructores de valores escalares SV a partir de diferentes tipos de fuentes: newSVuv para enteros sin signo, newSVnv para doubles, etc. (véase perlapi).

Como sabemos el recolector de basura de Perl usa un contador de referencias para saber que valores están en desuso y liberar la memoria correspondiente. La función sv_2mortal marca el SV recién creado como mortal para que su memoria pueda ser liberada por el sistema de gestión de memoria cuando sea seguro hacerlo.

Pruebas: Use Test::LectroTest

Use Test::LectroTest en la generación de las pruebas. Construya un generador de fracciones usando Test::LectroTest::Generator.



Subsecciones
Casiano Rodríguez León
2009-10-04