Todos sabemos (o deberíamos saber) que Haskell es vago por defecto. Nada se evalúa hasta que deba evaluarse. Entonces, ¿cuándo se debe evaluar algo? Hay puntos en los que Haskell debe ser estricto. A estos los llamo "puntos de rigor", aunque este término en particular no está tan extendido como había pensado. Según yo:
La reducción (o evaluación) en Haskell solo ocurre en puntos de rigor.
Entonces la pregunta es: ¿cuáles son, precisamente , los puntos de rigor de Haskell? Mi intuición dice que main
, los seq
patrones / bang, la coincidencia de patrones y cualquier IO
acción realizada a través de main
son los puntos de rigor primarios, pero realmente no sé por qué lo sé.
(También, si no son llamados "puntos de rigurosidad", lo que se llaman?)
Imagino que una buena respuesta incluirá alguna discusión sobre WHNF, etc. También imagino que podría tocar el cálculo lambda.
Editar: pensamientos adicionales sobre esta pregunta.
Como he reflexionado sobre esta pregunta, creo que sería más claro agregar algo a la definición de un punto de rigor. Los puntos de rigor pueden tener distintos contextos y una profundidad (o rigor) variable . Volviendo a mi definición de que "la reducción en Haskell solo ocurre en puntos de rigor", agreguemos a esa definición esta cláusula: "un punto de rigor solo se activa cuando su contexto circundante es evaluado o reducido".
Entonces, déjeme tratar de comenzar con el tipo de respuesta que quiero. main
es un punto de rigor. Está especialmente designado como el principal punto de rigor de su contexto: el programa. Cuando main
se evalúa el contexto del programa , se activa el punto de rigor de main. La profundidad de Main es máxima: debe evaluarse completamente. Main suele estar compuesto por acciones IO, que también son puntos de rigurosidad, cuyo contexto es main
.
Ahora intente: discuta seq
y empareje patrones en estos términos. Explique los matices de la aplicación de funciones: ¿cómo es estricta? ¿Cómo no es así? ¿Qué hay de deepseq
? let
y case
declaraciones? unsafePerformIO
? Debug.Trace
? ¿Definiciones de nivel superior? ¿Tipos de datos estrictos? ¿Patrones de explosión? Etc. ¿Cuántos de estos elementos se pueden describir en términos de coincidencia de patrones o secuencias?
fuente
seq
una coincidencia de patrones es suficiente, con el resto definido en términos de esos. Creo que la coincidencia de patrones asegura el rigor de lasIO
acciones, por ejemplo.+
en los tipos numéricos incorporados, también fuerzan el rigor, y supongo que lo mismo se aplica a las llamadas FFI puras.Respuestas:
Un buen lugar para comenzar es comprender este documento: Una semántica natural para la evaluación perezosa (Launchbury). Eso le dirá cuándo se evalúan las expresiones para un lenguaje pequeño similar al Core de GHC. Luego, la pregunta restante es cómo mapear Haskell completo a Core, y la mayor parte de esa traducción la proporciona el propio informe Haskell. En GHC llamamos a este proceso "desazúcar", porque elimina el azúcar sintáctico.
Bueno, esa no es toda la historia, porque GHC incluye toda una serie de optimizaciones entre el desugaring y la generación de código, y muchas de estas transformaciones reorganizarán el Core para que las cosas se evalúen en diferentes momentos (el análisis de rigurosidad en particular hará que las cosas se evalúen más temprano). Entonces, para comprender realmente cómo se evaluará su programa, debe mirar el Core producido por GHC.
Quizás esta respuesta le parezca un poco abstracta (no mencioné específicamente los patrones de explosión o seq), pero pidió algo preciso , y esto es lo mejor que podemos hacer.
fuente
Probablemente reformularía esta pregunta como: ¿Bajo qué circunstancias evaluará Haskell una expresión? (Quizás agregue una "forma normal a la cabeza débil").
En una primera aproximación, podemos especificar esto de la siguiente manera:
De su lista intuitiva, las acciones principales y de IO caen en la primera categoría, y la coincidencia de secuencias y patrones caen en la segunda categoría. Pero creo que la primera categoría está más en consonancia con su idea de "punto de rigor", porque así es como hacemos que la evaluación en Haskell se convierta en efectos observables para los usuarios.
Dar todos los detalles específicamente es una gran tarea, ya que Haskell es un lenguaje extenso. También es bastante sutil, porque Concurrent Haskell puede evaluar las cosas de manera especulativa, aunque al final no usemos el resultado: esta es una tercera clase de cosas que causan evaluación. La segunda categoría está bastante bien estudiada: desea observar el rigor de las funciones involucradas. La primera categoría también puede pensarse que es una especie de "rigurosidad", aunque esto es un poco cutre porque
evaluate x
yseq x $ return ()
en realidad son cosas diferentes! Puede tratarlo correctamente si le da algún tipo de semántica a la mónada IO (pasar explícitamente unRealWorld#
token funciona para casos simples), pero no sé si hay un nombre para este tipo de análisis de rigor estratificado en general.fuente
C tiene el concepto de puntos de secuencia , que son garantías para operaciones particulares de que un operando será evaluado antes que el otro. Creo que ese es el concepto existente más cercano, pero el término esencialmente equivalente punto de rigor (o posiblemente punto de fuerza ) está más en línea con el pensamiento de Haskell.
Entonces, su pensamiento sobre
!
/$!
yseq
es esencialmente correcto, pero la coincidencia de patrones está sujeta a reglas más sutiles. Siempre puede usar~
para forzar la coincidencia de patrones perezosos, por supuesto. Un punto interesante de ese mismo artículo:Continuemos por la madriguera del conejo y veamos los documentos para las optimizaciones realizadas por GHC:
En otras palabras, se puede generar código estricto en cualquier lugar como optimización, porque la creación de procesadores es innecesariamente costosa cuando los datos siempre serán necesarios (y / o solo se pueden usar una vez).
(Un término está en forma normal de cabeza si no hay beta-redex en la posición de cabeza 1. Un redex es un redex de cabeza si solo está precedido por abstractores lambda de no redexes 2 ). Entonces, cuando comienzas a forzar un thunk, estás trabajando en WHNF; cuando ya no quedan más golpes para forzar, estás en forma normal. Otro punto interesante:
Lo que implica, naturalmente, que, de hecho, cualquier
IO
acción realizada desdemain
hace evaluación de la fuerza, lo que debería ser obvio teniendo en cuenta que los programas de Haskell, de hecho, hacer las cosas. Todo lo que deba pasar por la secuencia definida enmain
debe estar en forma normal y, por lo tanto, está sujeto a una evaluación estricta.Sin embargo, CA McCann lo hizo bien en los comentarios: lo único que tiene de especial
main
es quemain
se define como especial; la coincidencia de patrones en el constructor es suficiente para asegurar la secuencia impuesta por laIO
mónada. En ese sentido, soloseq
y la coincidencia de patrones son fundamentales.fuente
Show
instancia del valor que se imprime.Haskell es AFAIK, no un lenguaje perezoso puro, sino un lenguaje no estricto. Esto significa que no necesariamente evalúa los términos en el último momento posible.
Puede encontrar una buena fuente del modelo de "pereza" de Haskell aquí: http://en.wikibooks.org/wiki/Haskell/Laziness
Básicamente, es importante comprender la diferencia entre un procesador y la forma normal de encabezado débil WHNF.
Tengo entendido que haskell hace cálculos al revés en comparación con los lenguajes imperativos. Lo que esto significa es que en ausencia de patrones "seq" y bang, en última instancia, será algún tipo de efecto secundario el que obligue a evaluar un thunk, lo que puede causar evaluaciones previas a su vez (verdadera pereza).
Como esto conduciría a una pérdida de espacio horrible, el compilador entonces averigua cómo y cuándo evaluar los procesadores antes de tiempo para ahorrar espacio. El programador puede apoyar este proceso proporcionando anotaciones de rigor (en.wikibooks.org/wiki/Haskell/Strictness, www.haskell.org/haskellwiki/Performance/Strictness) para reducir aún más el uso del espacio en forma de procesadores anidados.
No soy un experto en la semántica operativa de Haskell, por lo que dejaré el enlace como recurso.
Algunos recursos más:
http://www.haskell.org/haskellwiki/Performance/Laziness
http://www.haskell.org/haskellwiki/Haskell/Lazy_Evaluation
fuente
Perezoso no significa no hacer nada. Siempre que el patrón de su programa coincide con una
case
expresión, evalúa algo, lo suficiente de todos modos. De lo contrario, no puede averiguar qué RHS utilizar. ¿No ve ninguna expresión de caso en su código? No se preocupe, el compilador está traduciendo su código a una forma simplificada de Haskell donde es difícil evitar su uso.Para un principiante, una regla básica es que
let
es perezoso,case
es menos perezoso.fuente
case
siempre fuerza la evaluación en GHC Core, no lo hace en Haskell normal. Por ejemplo, inténtalocase undefined of _ -> 42
.case
en GHC Core evalúa su argumento a WHNF, mientras quecase
en Haskell evalúa su argumento tanto como sea necesario para seleccionar la rama adecuada. En el ejemplo de Hammar, eso no es en absoluto, perocase 1:undefined of x:y:z -> 42
evalúa más profundamente que WHNF.case something of (y,x) -> (x,y)
necesita evaluarsomething
en absoluto. Esto es cierto para todos los tipos de productos.something
tendría que ser evaluado en WHNF para alcanzar el constructor de tuplas.Esta no es una respuesta completa que apunte al karma, sino solo una pieza del rompecabezas; en la medida en que se trate de semántica, tenga en cuenta que existen múltiples estrategias de evaluación que proporcionan la misma semántica. Un buen ejemplo aquí, y el proyecto también habla de cómo pensamos típicamente de la semántica de Haskell, fue el proyecto Eager Haskell, que alteró radicalmente las estrategias de evaluación manteniendo la misma semántica: http://csg.csail.mit.edu/ pubs / haskell.html
fuente
El compilador de Glasgow Haskell traduce su código a un lenguaje similar al cálculo Lambda llamado core . En este lenguaje, algo va a ser evaluado, siempre que el patrón lo haga coincidir con una
case
declaración. Por lo tanto, si se llama a una función, se evaluará el constructor más externo y solo él (si no hay campos forzados). Todo lo demás está enlatado en un thunk. (Los thunks se introducen mediantelet
enlaces).Por supuesto, esto no es exactamente lo que sucede en el idioma real. El compilador convierte Haskell en Core de una manera muy sofisticada, haciendo que tantas cosas como sea posible sean perezosas y cualquier cosa que siempre se necesite perezosa. Además, hay valores sin caja y tuplas que siempre son estrictos.
Si intenta evaluar una función a mano, básicamente puede pensar:
fuente