¿Cuáles son los puntos de rigor de Haskell?

90

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 seqpatrones / bang, la coincidencia de patrones y cualquier IOacción realizada a través de mainson 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. maines un punto de rigor. Está especialmente designado como el principal punto de rigor de su contexto: el programa. Cuando mainse 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 seqy 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? lety casedeclaraciones? 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?

Dan Burton
fuente
10
Tu lista intuitiva probablemente no sea muy ortogonal. Sospecho que sequna coincidencia de patrones es suficiente, con el resto definido en términos de esos. Creo que la coincidencia de patrones asegura el rigor de las IOacciones, por ejemplo.
CA McCann
Las primitivas, como +en los tipos numéricos incorporados, también fuerzan el rigor, y supongo que lo mismo se aplica a las llamadas FFI puras.
Hammar
4
Parece que aquí se confunden dos conceptos. La coincidencia de patrones y los patrones seq y bang son formas en que una expresión puede volverse estricta en sus subexpresiones, es decir, si se evalúa la expresión superior, también lo es la subexpresión. Por otro lado, las principales acciones de ejecución de IO es cómo comienza la evaluación . Son cosas diferentes y es una especie de error incluirlas en la misma lista.
Chris Smith
@ChrisSmith No estoy tratando de confundir esos dos casos diferentes; en todo caso, estoy pidiendo más aclaraciones sobre cómo interactúan. La rigurosidad ocurre de alguna manera, y ambos casos son partes importantes, aunque diferentes, del rigor que "suceden". (y @ monadic: ಠ_ಠ)
Dan Burton
Si desea / necesita espacio para discutir aspectos de esta pregunta, sin intentar una respuesta completa, permítame sugerirle que utilice los comentarios en mi publicación / r / haskell para esta pregunta
Dan Burton

Respuestas:

46

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.

Simon Marlow
fuente
18
Siempre me ha parecido divertido que en lo que GHC llama "desugaring", el azúcar sintáctico que se elimina incluye la sintaxis real del propio lenguaje Haskell ... lo que implica, podría parecer, que GHC es en verdad un compilador optimizador para GHC Lenguaje central, que por cierto también incluye una interfaz muy elaborada para traducir Haskell a Core. :]
CA McCann
Sin embargo, los sistemas de tipos no se combinan con precisión ... en particular, pero no solo en lo que respecta a la traducción de clases de tipos en diccionarios explícitos, según recuerdo. Y todo lo último de TF / GADT, según tengo entendido, ha hecho que esa brecha sea aún más amplia.
sclv
GCC tampoco optimiza C: gcc.gnu.org/onlinedocs/gccint/Passes.html#Passes
György Andrasek
20

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:

  • La ejecución de acciones de IO evaluará cualquier expresión que "necesiten". (Por lo tanto, debe saber si se ejecuta la acción IO, por ejemplo, su nombre es main, o se llama desde main Y necesita saber qué necesita la acción).
  • Una expresión que se está evaluando (¡oye, esa es una definición recursiva!) Evaluará cualquier expresión que necesite.

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 xy seq 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 un RealWorld#token funciona para casos simples), pero no sé si hay un nombre para este tipo de análisis de rigor estratificado en general.

Edward Z. Yang
fuente
17

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.

En la práctica, Haskell no es un lenguaje puramente perezoso: por ejemplo, la coincidencia de patrones suele ser estricta (por lo que intentar una coincidencia de patrones obliga a que la evaluación se realice al menos lo suficientemente lejos como para aceptar o rechazar la coincidencia.

...

Los programadores también pueden usar la seqprimitiva para forzar una expresión a evaluar independientemente de si el resultado se usará alguna vez.

$!se define en términos de seq.

- Perezoso vs. no estricto .

Entonces, su pensamiento sobre !/ $!y seqes 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:

El analizador de rigor también busca casos en los que la expresión externa siempre requiere sub-expresiones y las convierte en una evaluación ansiosa. Puede hacer esto porque la semántica (en términos de "fondo") no cambia.

Continuemos por la madriguera del conejo y veamos los documentos para las optimizaciones realizadas por GHC:

El análisis de rigurosidad es un proceso mediante el cual GHC intenta determinar, en el momento de la compilación, qué datos definitivamente "siempre serán necesarios". Luego, GHC puede crear código para simplemente calcular dichos datos, en lugar del proceso normal (mayor sobrecarga) para almacenar el cálculo y ejecutarlo más tarde.

- Optimizaciones GHC: Análisis de rigurosidad .

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).

… No se puede realizar más evaluación sobre el valor; se dice que está en forma normal . Si estamos en alguno de los pasos intermedios de modo que hemos realizado al menos alguna evaluación de un valor, está en forma normal de cabeza débil (WHNF). (También hay una 'forma normal de cabeza', pero no se usa en Haskell). La evaluación completa de algo en WHNF lo reduce a algo en forma normal ...

- Wikilibros Haskell: Pereza

(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:

... si en algún momento necesitáramos, digamos, imprimir z al usuario, tendríamos que evaluarlo completamente ...

Lo que implica, naturalmente, que, de hecho, cualquier IOacción realizada desde main 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 en maindebe 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 maines que mainse define como especial; la coincidencia de patrones en el constructor es suficiente para asegurar la secuencia impuesta por la IOmónada. En ese sentido, solo seqy la coincidencia de patrones son fundamentales.

Jon Purdy
fuente
4
En realidad, la cita "si en algún momento necesitáramos, digamos, imprimir z para el usuario, tendríamos que evaluarla completamente" no es del todo correcta. Es tan estricto como la Showinstancia del valor que se imprime.
nominolo
10

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

fluida
fuente
6

Perezoso no significa no hacer nada. Siempre que el patrón de su programa coincide con una caseexpresió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 letes perezoso, casees menos perezoso.

T_S_
fuente
2
Tenga en cuenta que, si bien casesiempre fuerza la evaluación en GHC Core, no lo hace en Haskell normal. Por ejemplo, inténtalo case undefined of _ -> 42.
Hammar
2
caseen GHC Core evalúa su argumento a WHNF, mientras que caseen Haskell evalúa su argumento tanto como sea necesario para seleccionar la rama adecuada. En el ejemplo de Hammar, eso no es en absoluto, pero case 1:undefined of x:y:z -> 42evalúa más profundamente que WHNF.
Max
Y tampoco case something of (y,x) -> (x,y)necesita evaluar somethingen absoluto. Esto es cierto para todos los tipos de productos.
Ingo
@Ingo - eso es incorrecto. somethingtendría que ser evaluado en WHNF para alcanzar el constructor de tuplas.
John L
John - ¿Por qué? Sabemos que debe ser una tupla, entonces, ¿cuál es el punto de evaluarlo? Es suficiente si xey están vinculados al código que evalúa la tupla y extrae la ranura adecuada, en caso de que alguna vez se necesiten.
Ingo
4

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

sclv
fuente
2

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 casedeclaració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 mediante letenlaces).

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:

  • Intente evaluar el constructor más externo de la devolución.
  • Si se necesita algo más para obtener el resultado (pero solo si es realmente necesario), también se evaluará. El orden no importa.
  • En el caso de IO, debe evaluar los resultados de todas las declaraciones desde la primera hasta la última en eso. Esto es un poco más complicado, ya que la mónada IO hace algunos trucos para forzar la evaluación en un orden específico.
fuz
fuente