Una Introducción agradable a Haskell
anterior siguiente inicio


10  Números

Haskell proporciona una rica colección de tipos numéricos basados en los definidos en el lenguaje Scheme[7], los cuales a su vez están basados en los del lenguaje Common Lisp [8]. (Estos lenguajes, sin embargo, tienen tipado dinámico). Los tipos estándar incluyen enteros de precisión fija y arbitraria, ratios y números racionales formados desde cada tipo entero, así como reales y complejos de simple y doble precisión. Nos referiremos en este apartado a las características básicas de las estructuras de clases numéricas; para más detalles consúltese (§6.4).

10.1  Estructura de las Clases Numéricas

Las clases numéricas (la clase Num y aquellas que caen bajo ella) abarcan la mayoría de las clases estándar. También se observa que Num es una subclase de Eq, pero no de Ord; esto se debe a que la ordenación no tiene sentido para ciertos números, como los complejos. La subclase Real de Num es, sin embargo, una subclase de Ord.

La clase Num proporciona las operaciones básicas comunes a todos los tipos numéricos: éstas incluyen, entre otras, la adición, substracción, negación, multiplicación y el valor absoluto:

     (+),(-),(*)  :: (Num a) => a -> a -> a
     negate, abs  :: (Num a) => a -> a

[negate es una función que se utiliza como el operador prefijo menos; no podemos utilizar para ello (-), porque esta es la función de substracción. Por ejemplo, -x*y es equivalente a negate (x*y). (El menos prefijo tiene la misma precedencia que el menos infijo, la cual, por supuesto, es menor que la de la multiplicación.)]

Obsérvese que la clase Num no proporciona la operación de división; en dos subclases de Num se proporcionarán versiones diferentes de este operador de división.

La clase Integral proporciona una división entera y un resto de la división. Las instancias de Integral son Integer (una versión de enteros no limitados, también conocidas como "números grandes") e Int (limitados a la representación fija de la máquina para los enteros, con un rango equivalente al menos a 29 dígitos binarios más el signo).  Una implementación particular de Haskell puede proveer otros tipos integrales además de estos. Obsérvese que Integral es una subclase de Real, en lugar de serlo de  Num directamente; esto quiere decir que no hay intención de proporicionar enteros Gausianos.

Todos los demás tipos numéricos caen en la clase Fractional, la cual proporciona la operación de división ordinaria (/). La subclase Floating proporciona funciones trigonométricas, logarítmicas y exponenciales.

La subclase RealFrac de Fractional y Real proporciona una función properFraction, la cual descompone un número en su parte entera y fraccionaria, y una colección de funciones que redondean a integral valores de diferentes formas:

     properFraction         :: (Fractional a , Integral b) => a -> (a,b)
     truncate, round,
     floor, ceiling         :: (Fractional a , Integral b) => a -> b

La subclase RealFloat de Floating y RealFrac proporciona algunas funciones especializadas para el acceso a los componentes de un número flotante, el exponente y la mantisa. Los tipos Float y Double caen dentro de la clase RealFloat.

10.2  Constructores de Números

De los tipos numéricos estándar, Int, Integer, Float y Double son primitivos. Los otros se construyen a partir de estos.

Complex (se encuentra en la librería Complex) es un constructor de tipo que genera un dato complejo en la clase Floating a partir de datos de tipo RealFloat.

     data (RealFloat a) => Complex a = !a :+ !a  deriving (Eq,Show)

El símbolo ! es una anotación que hace al dato estricto; esto se discute en la Sección 6.3. Obsérvese que (RealFloat a) restringe el tipo del argumento; así, los complejos estándar tienen el tipo Complex Float y Complex Double. Desde la declaración data podemos ver que un número complejo se escribe como x :+ y; los argumentos definen la parte real e imaginaria respectivamente. Al ser:+ un constructor de datos, podemos usarlo como patrón:

     conjugate            :: (RealFloat a) => Complex a -> Complex a
     conjugate (x :+ y)   =  x :+ (-y)

Similarmente, el tipo Ratio (se encuentra en la librería Rational) crea un tipo racional en la clase RealFrac  desde instancias de tipo Integral. (Rational es un sinónimo de tipos para Ratio Integer). Ratio, sin embargo, es un constructor de tipos abstracto. En lugar de un constructor como :+, los racionales usan la función % para formar un ratio entre dos enteros. En lugar de utilizar patrones, los componentes pueden extraerse a través de funciones:

     (%)                    :: (Integral a) => a -> a -> Ratio a
     numerator,denominator  :: (Integral a) => Ratio a -> a

¿A qué se debe esta diferencia?. Los números complejos en forma cartesiana tiene una representación única. Por otro lado, los ratios no tiene representación única, pero tienen una forma canónica (forma reducida) que la implementación del tipo abstracto debe mantener; no es verdad que el valor de  numerator (x%y) sea x, pero si es verdad que la parte real de x+:y es x.

10.3  Promociones Numéricas y Literales Sobrecargados

El Prelude y las librerías proporcionan varias funciones sobrecargadas que pueden utilizarse para la promoción explícita:

     fromInteger         :: (Num a) => Integer -> a
     fromRational        :: (Fractional a) => Rational -> a
     toInteger           :: (Integral a) => a -> Integer
     toRational          :: (RealFrac a) => a -> Rational
     fromIntegral        :: (Integral a, Num b) => a -> b
     fromRealFrac        :: (RealFrac a, Fractional b) => a -> b
     fromIntegral        =  fromInteger . toInteger
     fromRealFrac        =  fromRational . toRational

Dos de éstas se usan de forma implícita para la sobrecarga de literales: un número entero (sin punto decimal) es equivalente a la aplicación de fromInteger al valor del número como Integer. Similarmente, un numero flotante (con un punto decimal) es visto como una aplicación de fromRational al valor del número como Rational. Es decir, 7 tiene el tipo (Num a) = > a, y 7.3 tiene el tipo (Fractional a) => a. Esto quiere decir que podemos utilizar literales numéricos en funciones numéricas genéricas, por ejemplo:

     halve               :: (Fractional a) => a -> a
     halve x             = x * 0.5

Esta forma de sobrecargar los números tiene la ventaja adicional de que el método para interpretar un literal numérico como un número de un tipo dado puede ser especificado en la declaración de la instancia Integral o Fractional (ya que fromInteger y fromFractional son operadores de estas clases, respectivamente). Por ejemplo, la instancia Num de (RealFloat a) => Complex a contiene el método:

     fromInteger x      = fromInteger x:+ 0

Esto nos dice que fromInteger está definida en Complex para producir un número complejo cuya parte real es proporcionada por la función fromInteger de la clase RealFloat. De esta manera, cada tipo definido por el usuario (por ejemplo, cuaterniones) puede hacer uso de los literales sobrecargados.

Como otro ejemplo, volvemos a ver la definición de inc de la Sección 2:

     inc :: Integer -> Integer
     inc n = n+1

Ignorando la declaración de tipo, el tipo más general de inc es (Num a) => a -> a. La declaración de tipo explícita es legal pues es más específica que el tipo principal (uno más general causaría un error de tipos). La declaración de tipo tiene el efecto de restringir el tipo de inc y en este caso, algo como inc (1::Float) tendría un tipo erróneo.

10.4  Tipos Numéricos por Defecto

Considérese la siguiente definición de función:

     rms            :: (Floating a) => a -> a -> a
     rms x y        = sqrt ((x^2 + y^2)*0.5)

La función de exponenciación (^) (una de las tres formas diferentes de exponenciación en Haskell, véase §6.8.5) tiene el tipo (Num a, Integral b) => a -> b -> a, y, siendo 2 de tipo (Num a) => a, el tipo de x^2 es (Num a, Integral b) => a. Esto es un problema; no hay forma de resolver la sobrecarga asociada con el tipo variable b, que aparece en el contexto aunque no se utiliza en la expresión de tipo. Esencialmente, el programador ha especificado que la x debe elevarse al cuadrado pero no a cual de los dos tipos, Int o Integer pertenece el exponente. Por supuesto, podemos fijar esto:

     rms x y        = sqrt ((x^(2::Integer) + y^(2::Integer))*0.5)

Sin embargo, es obvio que este tipo de soluciones son fastidiosas. Por otro lado, este tipo de ambigüedad en la sobrecarga no ocurre sólo en los números:

     show (read "xyz")

¿Qué tipo tiene la cadena que se ha leído? Este problema es más serio que la ambigüedad en la exponenciación, porque allí, cualquier instancia de la clase Integral vale, mientras que aquí se pueden esperar comportamientos muy distintos dependiendo de la instancia de Show que se use para resolver la ambigüedad.

Debido a la diferencia entre los casos numéricos y generales en lo que se refiere a la ambigüedad de la sobrecarga, Haskell proporciona una solución restringida al caso numérico: cada módulo puede contener una declaración por defecto, consistente en la palabra clave default seguida de una serie de tipos monomórficos numéricos (tipos sin variables) separados por comas y encerrados entre paréntesis. Cuando una variable de  tipo es ambigua, se consulta la lista por defecto, y se usa el primer tipo de la lista que satisface el contexto de esa variable de tipo. Por ejemplo, si la declaración por defecto es default (Int,Float), el exponente ambiguo del ejemplo anterior será resuelto al tipo Int. (ver §4.3.4 para más detalles).

El valor por defecto de la clave default (si no se especifica nada) es (Integer,Double), aunque (Integer, Rational, Double) podría también haber sido apropiado. Muchos programadores cautos preferirán usar default () que no provee ningún valor por defecto.


Una Introducción agradable a Haskell
anterior siguiente inicio