¿Qué significa el signo de exclamación en una declaración de Haskell?

262

Me encontré con la siguiente definición cuando trato de aprender Haskell usando un proyecto real para conducirlo. No entiendo lo que significa el signo de exclamación frente a cada argumento y mis libros no parecen mencionarlo.

data MidiMessage = MidiMessage !Int !MidiMessage
David
fuente
13
Sospecho que esta podría ser una pregunta muy común; Recuerdo claramente haberme preguntado exactamente lo mismo, hace mucho tiempo.
cjs

Respuestas:

314

Es una declaración de rigor. Básicamente, significa que debe evaluarse a lo que se llama "forma normal de cabeza débil" cuando se crea el valor de la estructura de datos. Veamos un ejemplo, para que podamos ver lo que esto significa:

data Foo = Foo Int Int !Int !(Maybe Int)

f = Foo (2+2) (3+3) (4+4) (Just (5+5))

La función f anterior, cuando se evalúa, devolverá un "thunk": es decir, el código a ejecutar para calcular su valor. En ese momento, un Foo aún no existe, solo el código.

Pero en algún momento alguien puede intentar mirar dentro de él, probablemente a través de una coincidencia de patrones:

case f of
     Foo 0 _ _ _ -> "first arg is zero"
     _           -> "first arge is something else"

Esto va a ejecutar suficiente código para hacer lo que necesita, y nada más. Por lo tanto, creará un Foo con cuatro parámetros (porque no puede mirar dentro sin que exista). El primero, ya que lo estamos probando, necesitamos evaluar todo el camino para4 que nos demos cuenta de que no coincide.

El segundo no necesita ser evaluado, porque no lo estamos probando. Por lo tanto, en lugar de 6ser almacenado en esa ubicación de memoria, sólo tendremos que almacenar el código para una posible evaluación posterior, (3+3). Eso se convertirá en un 6 solo si alguien lo mira.

Sin embargo, el tercer parámetro tiene un !frente, por lo que se evalúa estrictamente: (4+4)se ejecuta y8 se almacena en esa ubicación de memoria.

El cuarto parámetro también se evalúa estrictamente. Pero aquí es donde se vuelve un poco complicado: estamos evaluando no completamente, sino solo con una forma de cabeza normal débil. Esto significa que descubrimos si es Nothingo no Just, y lo almacenamos, pero no vamos más allá. Eso significa que no almacenamos Just 10pero en realidadJust (5+5) , dejamos el thunk adentro sin evaluar. Es importante saberlo, aunque creo que todas las implicaciones de esto van más allá del alcance de esta pregunta.

Puede anotar argumentos de función de la misma manera, si habilita la BangPatternsextensión de idioma:

f x !y = x*y

f (1+1) (2+2)devolverá el golpe (1+1)*4.

cjs
fuente
16
Esto es muy útil. Sin embargo, no puedo evitar preguntarme si la terminología de Haskell está haciendo las cosas más complicadas para que la gente las entienda con términos como "forma de cabeza normal débil", "estricto", etc. Si te entiendo correctamente, ¡suena como el! operador simplemente significa almacenar el valor evaluado de una expresión en lugar de almacenar un bloque anónimo para evaluarlo más tarde. ¿Es una interpretación razonable o hay algo más?
David
71
@David La pregunta es hasta qué punto evaluar el valor. Forma normal de cabeza débil significa: evaluarlo hasta llegar al constructor más externo. La forma normal significa evaluar todo el valor hasta que no queden componentes sin evaluar. Debido a que Haskell permite todo tipo de niveles de profundidad de evaluación, tiene una terminología rica para describir esto. No tiende a encontrar estas distinciones en idiomas que solo admiten semántica de llamada por valor.
Don Stewart
8
@David: Escribí una explicación más detallada aquí: Haskell: ¿Qué es la forma normal de la cabeza débil? . Aunque no menciono los patrones de explosión o las anotaciones de rigor, son equivalentes al uso seq.
hammar
1
Solo para estar seguro de que entendí: ¿esa pereza se relaciona solo con argumentos desconocidos en tiempo de compilación? Es decir, si el código fuente realmente tiene una declaración (2 + 2) , ¿aún estaría optimizado a 4 en lugar de tener un código para agregar los números conocidos?
Hi-Angel
1
Hola Angel, eso depende realmente de hasta qué punto el compilador quiere optimizar. Aunque usamos flequillo para decir que nos gustaría forzar ciertas cosas a debilitar la forma normal de la cabeza, no tenemos (y tampoco necesitamos o queremos) una forma de decir "por favor, asegúrese de que aún no evalúe este thunk Un paso más." Desde un punto de vista semántico, dado un thunk "n = 2 + 2", ni siquiera se puede saber si alguna referencia particular a n se está evaluando ahora o si se evaluó previamente.
cjs
92

Una manera simple de ver la diferencia entre argumentos de constructor estrictos y no estrictos es cómo se comportan cuando no están definidos. Dado

data Foo = Foo Int !Int

first (Foo x _) = x
second (Foo _ y) = y

Dado que el argumento no estricto no es evaluado por second, pasar undefinedno causa un problema:

> second (Foo undefined 1)
1

Pero el argumento estricto no puede ser undefined, incluso si no usamos el valor:

> first (Foo 1 undefined)
*** Exception: Prelude.undefined
Chris Conway
fuente
2
Esto es mejor que la respuesta de Curt Sampson porque en realidad explica los efectos observables por el usuario que !tiene el símbolo, en lugar de profundizar en los detalles de implementación internos.
David Grayson
26

Creo que es una anotación estricta.

Haskell es un lenguaje funcional puro y vago , pero a veces la sobrecarga de la pereza puede ser demasiado o un desperdicio. Entonces, para lidiar con eso, puede pedirle al compilador que evalúe completamente los argumentos de una función en lugar de analizar los thunks.

Hay más información en esta página: rendimiento / rigidez .

Chris Vest
fuente
¡Hmm, el ejemplo en esa página que mencionaste parece hablar sobre el uso de! al invocar una función. Pero eso parece diferente a ponerlo en una declaración de tipo. ¿Qué me estoy perdiendo?
David
Crear una instancia de un tipo también es una expresión. Puede pensar en los constructores de tipos como funciones que devuelven nuevas instancias del tipo especificado, dados los argumentos proporcionados.
Chris Vest
44
De hecho, para todos los efectos, los constructores de tipos son funciones; puede aplicarlos parcialmente, pasarlos a otras funciones (por ejemplo, map Just [1,2,3]para obtener [Solo 1, Solo 2, Solo 3]) y así sucesivamente. Me resulta útil pensar en la capacidad de combinar patrones con ellos, así como en una instalación completamente no relacionada.
cjs