Alcance y flotadores Haskell
¿Por qué el comportamiento de la notación de rango de Haskell es diferente para flotantes que para números enteros y caracteres?
Prelude> [1, 3 .. 10] :: [Int]
[1,3,5,7,9]
Prelude> [1, 3 .. 10] :: [Float]
[1.0,3.0,5.0,7.0,9.0,11.0]
Prelude> ['a', 'c' .. 'f']
"ace"
Lo entendería si el último elemento estuviera cerca del límite superior, pero obviamente esto no es una cuestión de redondeo.
La sintaxis [e1, e2 .. e3]
es realmente azúcar sintáctica para enumFromThenTo e1 e2 e3
, que es una función en la Enum
clase de tipos.
El estándar Haskell define su semántica de la siguiente manera:
Para los tipos
Int
yInteger
, las funciones de enumeración tienen el siguiente significado:
- La secuencia
enumFrom e1
es la lista[e1,e1 + 1,e1 + 2,…]
.- La secuencia
enumFromThen e1 e2
es la lista[e1,e1 + i,e1 + 2i,…]
, donde el incremento,i
ese2 − e1
. El incremento puede ser cero o negativo. Si el incremento es cero, todos los elementos de la lista son iguales.- La secuencia
enumFromTo e1 e3
es la lista[e1,e1 + 1,e1 + 2,…e3]
. La lista está vacía sie1 > e3
.- La secuencia
enumFromThenTo e1 e2 e3
es la lista[e1,e1 + i,e1 + 2i,…e3]
, donde el incremento,i
ese2 − e1
. Si el incremento es positivo o cero, la lista termina cuando el siguiente elemento sea mayor quee3
; la lista está vacía sie1 > e3
. Si el incremento es negativo, la lista termina cuando el siguiente elemento sea menor quee3
; la lista está vacía sie1 < e3
.
Esto es más o menos lo que cabría esperar, pero las instancias Float
y Double
se definen de forma diferente:
Para
Float
yDouble
, la semántica de laenumFrom
familia viene dada por las reglasInt
anteriores, excepto que la lista termina cuando los elementos se vuelven mayores quee3 + i∕2
para el incremento positivoi
, o cuando se vuelven menores quee3 + i∕2
para el negativoi
.
No estoy realmente seguro de cuál es la justificación para esto, así que la única respuesta que puedo darle es que es así porque está definido así en el estándar.
Puede solucionar este problema enumerando el uso de números enteros y convirtiéndolos Float
posteriormente.
Prelude> map fromIntegral [1, 3 .. 10] :: [Float]
[1.0,3.0,5.0,7.0,9.0]
Ok, @Henning Makholm ya dijo esto en su comentario, pero no explicó por qué en realidad es una mejor solución.
Lo primero que debemos decir: cuando trabajamos con punto flotante, siempre debemos estar atentos a los posibles errores de redondeo. Cuando escribamos [0.0, 0.1 .. 1.0]
debemos ser conscientes de que todos estos números, excepto el primero, no estarán en el lugar exacto de las décimas. Cuando necesitemos este tipo de certeza, no debemos utilizar flotadores en absoluto.
Pero, por supuesto, hay muchas aplicaciones en las que nos contentamos con una certeza razonable , pero necesitamos alta velocidad. Ahí es donde las carrozas son geniales. Una posible aplicación de dicha lista sería una integración numérica trapezoidal simple:
trIntegrate f l r s = sum [ f x | x<-[l,(l+s)..r] ] * s - (f(l)+f(r))*s/2
Probemos esto: trIntegrate ( \x -> exp(x + cos(sqrt(x) - x*x)) ) 1.0 3.0 0.1
=> 25,797334337026466
comparado con 25,9144, un error de menos del uno por ciento. No es exacto, por supuesto, pero eso es inherente al método de integración.
Supongamos ahora que los rangos flotantes se definieron para terminar siempre al cruzar el borde derecho. Entonces, sería posible (¡pero no podemos estar seguros de ello!) que solo se calculen 20 valores en lugar de 21 en la suma, porque el último valor de x
resulta ser 3,000000 y tantos. Podemos simular esto
bad_trIntegrate f l r s = sum [ f x | x<-[l,(l+s)..(r-s)] ] * s - (f(l)+f(r))*s/2
entonces obtenemos
bad_trIntegrate ( \x -> exp(x + cos(sqrt(x) - x*x)) ) 1.0 3.0 0.1
=> 21.27550564546988
¡Urgh!
Esto no tiene nada que ver con ocultar los problemas del punto flotante. Es sólo un método para ayudar al programador a solucionar estos problemas más fácilmente. De hecho, ¡el resultado contradictorio de [1, 3 .. 10] :: Float
ayuda a recordar estos problemas!