haskell - Int vs Word de uso común?
type-safety (3)
Parece que el patrón común de tomar / devolver Int
(es decir, ByteString.hGet
y Data.List.length
) es contrario al patrón de Haskell de utilizar tipos que describen mucho, ya que muchos de estos casos solo pueden manejar números positivos. ¿No sería mejor usar Word
o hay alguna razón por la cual estas funciones sean parciales en Int
?
El mismo razonamiento se aplica como en C. La razón para usar tipos más precisos es para evitar errores. Errores, en este caso, como tratar de usar números negativos donde no son significativos. Pero el comportamiento de Word
en over- o underflow, como unsigned int
en C, es para envolverlo. Si intenta utilizar un número negativo donde se espera una Word
(o unsigned int
), no obtendrá el compilador que le grita, o incluso una excepción en el tiempo de ejecución. Obtienes un gran número positivo. ¡Lo cual no se puede distinguir de ningún otro número positivo grande ("legítimo")!
Mira este:
Prelude Data.Word> -1 :: Word
4294967295
En lugar de cometer errores imposibles de cometer, los ha hecho imposibles de detectar. Con Int
(y int
), al menos tiene la posibilidad de verificar valores negativos manualmente. Con Word
y unsigned int
, no tienes nada.
Lo que sería valioso es un tipo sin firmar que reacciona ante un exceso o un defecto al lanzar una excepción. Eso todavía no haría los errores imposibles, pero los haría más fáciles de detectar. Sin embargo, vendría a un costo de rendimiento. * No sé si es posible descartarlos en tiempo de compilación, pero no parece fácil.
* Al menos, x86 requiere una instrucción adicional, ¡después de cada operación! - para comprobar si se produjo un exceso o defecto de flujo. No sé si hay una arquitectura que lo haga "gratis", aunque sería agradable. O tal vez un valor de NaN distinguido como el que tenemos para los números de coma flotante (quizás en lugar del número más negativo ) que se usaría para denotar valores irrepresentables ...
Es cierto que la expresividad del sistema de tipo Haskell alienta a los usuarios a asignar tipos precisos a las entidades que definen. Sin embargo, los experimentados Haskeller reconocerán fácilmente que se debe lograr un equilibrio entre la precisión máxima del tipo (que además no siempre es posible dados los límites actuales del sistema de tipo Haskell) y la conveniencia. En resumen, los tipos precisos solo son útiles para un punto. Más allá de ese punto, a menudo solo causan burocracia adicional con poco o ningún beneficio.
Vamos a ilustrar el problema con un ejemplo. Considera la función factorial. Para todos n
mayor que 1, el factorial de n
es un número par, y el factorial de 1 no es terriblemente interesante, así que ignorémoslo. Por lo tanto, para asegurarnos de que nuestra implementación de la función factorial en Haskell es correcta, podríamos sentirnos tentados de introducir un nuevo tipo numérico, que solo puede representar números enteros sin signo:
module (Even) where
newtype Even = Even Integer
instance Num Even where
...
fromInteger x | x `mod` 2 == 0 = Even x
| otherwise = error "Not an even number."
instance Integral Even where
...
toInteger (Even x) = x
Sellamos este tipo de datos dentro de un módulo que no exporta el constructor, para hacerlo abstracto, y lo hacemos una instancia de todas las clases de tipos relevantes que Int
es una instancia de. Ahora podemos dar la siguiente firma a factorial:
factorial :: Int -> Even
El tipo de factorial
seguro es más preciso que si dijéramos que devuelve Int
. Pero descubrirá que la definición factorial
con ese tipo es realmente bastante molesta, porque necesita una versión de multiplicación que multiplica un Int
(par o impar) con un Even
y produce y Even
. Además, es posible que tenga que introducir llamadas extrañas a toInteger
sobre el resultado de una llamada a factorial
en el código del cliente, que puede ser una fuente importante de ruido y desorden para obtener poca ganancia. Además, todas estas funciones de conversión podrían tener un impacto negativo en el rendimiento.
Otro problema es que al introducir un tipo nuevo y más preciso, a menudo terminas teniendo que duplicar todo tipo de funciones de la biblioteca. Por ejemplo, si introduce el tipo List1 a
de listas no vacías, tendrá que volver a implementar muchas de las funciones que ya proporciona Data.List
, pero solo para [a]
. Claro, uno puede hacer que estas funciones sean métodos de la clase de tipo ListLike
. Pero rápidamente terminas con todo tipo de clases de tipo adhoc y otro repetitivo, con nuevamente poca ganancia.
Un último punto es que uno no debería considerar Word
como una variante sin firma de Int
. El informe Haskell deja el tamaño real de Int
no especificado, y solo garantiza que este tipo sea capaz de representar números enteros en el rango [- 2 29 , 2 29 - 1]. Se dice que el tipo Word
proporciona enteros sin signo de ancho no especificado. No se garantiza que en una implementación conforme el ancho de una Word
corresponda al ancho de un Int
.
Aunque hago una defensa contra la excesiva proliferación de tipos, reconozco que la introducción de un tipo de Natural
de los naturales podría ser agradable. Sin embargo, en última instancia, si Haskell debería tener un tipo dedicado para números naturales, además de Int
, Integer
y los diversos tipos de Word*
, es en gran medida una cuestión de gusto. Y el estado actual de las cosas es probablemente en gran parte solo un accidente de la historia.
Mi primera suposición es que la aritmética sin signo tiene algunos problemas que podrían causar errores estúpidos si no estás prestando atención:
Prelude Data.Word> let x = 0 :: Word in if x - 1 > x then "Ouch" else "Ohayoo"
"Ouch"
Tendrá algunos problemas en las funciones polimórficas que parecen ser correctos:
Prelude Data.Word> let f acc x y = if x <= y then f (acc + 1) x (y - 1) else acc
Prelude Data.Word> :t f
f :: (Num a1, Num a, Ord a) => a1 -> a -> a -> a1
Usando una función de longitud estándar:
Prelude Data.Word> let len'' x = if null x then 0 else 1 + len'' (tail x) :: Int
Prelude Data.Word> :t len''
len'' :: [a] -> Int
Prelude Data.Word> f 0 0 (len'' [1,2,3])
4
Y usando una función de longitud returing Word
:
Prelude Data.Word> let len x = if null x then 0 else 1 + len (tail x) :: Word
Prelude Data.Word> :t len
len :: [a] -> Word
Prelude Data.Word> f 0 0 (len [1,2,3])
...diverges...
Y, por supuesto, si esas funciones devuelven Word
lugar de Int
, introduces la necesidad de seguir transformando tus Word
en Int
s para usarlo en otros lugares comunes.