¿Qué reglas hay sobre una función a -> () que se evalúa en Haskell?

12

Tal como dice el título: ¿qué garantías hay para evaluar una unidad de retorno de la función Haskell? Uno pensaría que no es necesario ejecutar ningún tipo de evaluación en tal caso, el compilador podría reemplazar todas esas llamadas con un ()valor inmediato a menos que existan solicitudes explícitas de rigor, en cuyo caso el código podría tener que decidir si debería retorno ()o fondo.
He experimentado con esto en GHCi, y parece que sucede lo contrario, es decir, tal función parece ser evaluada. Un ejemplo muy primitivo sería

f :: a -> ()
f _ = undefined

La evaluación f 1arroja un error debido a la presencia de undefined, por lo que definitivamente ocurre alguna evaluación. Sin embargo, no está claro cuán profunda es la evaluación; a veces parece ir tan profundo como es necesario evaluar todas las llamadas a funciones que regresan (). Ejemplo:

g :: [a] -> ()
g [] = ()
g (_:xs) = g xs

Este código se repite indefinidamente si se presenta con g (let x = 1:x in x). Pero entonces

f :: a -> ()
f _ = undefined
h :: a -> ()
h _ = ()

se puede usar para mostrar que h (f 1)devuelve (), por lo que en este caso no se evalúan todas las subexpresiones de valores unitarios. ¿Cuál es la regla general aquí?

ETA: por supuesto que sé sobre la pereza. Me pregunto qué impide que los escritores de compiladores hagan que este caso en particular sea aún más vago de lo habitual.

ETA2: resumen de los ejemplos: GHC parece tratar ()exactamente como cualquier otro tipo, es decir, como si hubiera una pregunta sobre qué valor regular que habita el tipo debe devolverse de una función. El hecho de que solo haya uno de esos valores no parece ser (ab) utilizado por los algoritmos de optimización.

ETA3: cuando digo Haskell, me refiero a Haskell según lo definido por el Informe, no a Haskell-the-H-in-GHC. Parece ser una suposición no compartida tan ampliamente como imaginé (que era 'por el 100% de los lectores'), o probablemente habría podido formular una pregunta más clara. Aun así, lamento haber cambiado el título de la pregunta, ya que originalmente preguntaba qué garantías hay para que se llame a tal función.

ETA4: parece que esta pregunta ha seguido su curso, y lo estoy considerando sin respuesta. (Estaba buscando una función de 'pregunta cerrada' pero solo encontré 'responde tu propia pregunta' y como no se puede responder, no tomé esa ruta). Nadie sacó nada del Informe que lo decidiera de cualquier manera , que me siento tentado a interpretar como una fuerte pero no definitiva respuesta "sin garantía para el idioma como tal". Todo lo que sabemos es que la implementación actual de GHC no omitirá la evaluación de dicha función.

Me he encontrado con el problema real al portar una aplicación OCaml a Haskell. La aplicación original tenía una estructura recursiva de muchos tipos, y el código declaraba una serie de funciones que requerían assert_structureN_is_correctN en 1..6 o 7, cada una de las cuales devolvía la unidad si la estructura era realmente correcta y lanzaba una excepción si no era así. . Además, estas funciones se llamaban entre sí al descomponer las condiciones de corrección. En Haskell, esto se maneja mejor con la Either Stringmónada, así que lo transcribí de esa manera, pero la pregunta como cuestión teórica permaneció. Gracias por todas las entradas y respuestas.

Criptozoon
fuente
1
Esto es pereza en el trabajo. A menos que se exija el resultado de una función (por ejemplo, mediante la coincidencia de patrones con un constructor), el cuerpo de la función no se evalúa. Para observar la diferencia, intente comparar h1::()->() ; h1 () = ()y h2::()->() ; h2 _ = (). Ejecute ambos h1 (f 1)y h2 (f 1), y vea que solo el primero exige (f 1).
chi
1
"La pereza parece dictar que se reemplaza con () sin que ocurra ningún tipo de evaluación". Qué significa eso? f 1es "reemplazado" por undefineden todos los casos.
oisdk
3
Una función ... -> ()puede 1) terminar y regresar (), 2) terminar con un error de excepción / tiempo de ejecución y no devolver nada, o 3) divergir (recursión infinita). GHC no optimiza el código, suponiendo que solo 1) puede suceder: si f 1se solicita, no omite su evaluación y devolución (). La semántica de Haskell es evaluarla y ver qué sucede entre 1,2,3.
chi
2
Realmente no hay nada especial sobre ()(el tipo o el valor) en esta pregunta. Todas las mismas observaciones suceden si reemplazas () :: ()con, digamos, en 0 :: Inttodas partes. Todas estas son viejas y aburridas consecuencias de la evaluación perezosa.
Daniel Wagner
2
no, "evitar", etc., no es la semántica de Haskell. y hay dos valores posibles de ()tipo, ()y undefined.
Will Ness

Respuestas:

10

Parece suponer que el tipo ()tiene un solo valor posible ()y, por lo tanto, espera que cualquier llamada de función que devuelva un valor de tipo se ()suponga automáticamente que realmente produce el valor ().

Así no es como funciona Haskell. Cada tipo tiene un valor más en Haskell, es decir, ningún valor, error o el llamado "fondo", codificado por undefined. Por lo tanto, se está llevando a cabo una evaluación:

main = print (f 1)

es equivalente al lenguaje principal

main = _Case (f 1) _Of x -> print x   -- pseudocode illustration

o incluso (*)

main = _Case (f 1) _Of x -> putStr "()"

y Core's _Caseestá forzando :

"La evaluación de una %case[expresión] fuerza la evaluación de la expresión que se está probando (el" escrutinio "). El valor del escrutinio está vinculado a la variable que sigue a la %ofpalabra clave, ...".

El valor se ve obligado a la forma normal de la cabeza débil. Esto es parte de la definición del lenguaje.

Haskell no es un lenguaje de programación declarativo .


(*) print x = putStr (show x) y show () = "()", por lo que la showllamada se puede compilar por completo.

De hecho, el valor se conoce de antemano como (), e incluso el valor de show ()se conoce de antemano como "()". Aún así, la semántica de Haskell aceptada exige que el valor de (f 1)se vea forzado a la forma normal de la cabeza débil antes de proceder a imprimir esa cadena conocida de antemano "()".


editar: considerar concat (repeat []). ¿Debería ser [], o debería ser un bucle infinito?

La respuesta de un "lenguaje declarativo" a esto es muy probable []. La respuesta de Haskell es, el bucle infinito .

La pereza es la "programación declarativa del pobre", pero todavía no es la realidad .

edit2 : print $ h (f 1) == _Case (h (f 1)) _Of () -> print ()y solo hes forzado, no f; y para producir su respuesta hno tiene que forzar nada, según su definición h _ = ().

Observaciones de despedida: La pereza puede tener razón de ser, pero no es su definición. La pereza es lo que es. Se define como todos los valores que inicialmente son thunks que son forzados a WHNF de acuerdo con las demandas que provienen main. Si ayuda a evitar el fondo en un determinado caso específico de acuerdo con sus circunstancias específicas, entonces lo hace. Si no, no. Eso es todo.

Ayuda a implementarlo usted mismo, en su idioma favorito, para tener una idea de ello. Pero también podemos rastrear la evaluación de cualquier expresión nombrando cuidadosamente todos los valores intermedios a medida que surgen.


A juzgar por el informe , tenemos

f :: a -> ()
f = \_ -> (undefined :: ())

entonces

print (f 1)
 = print ((\ _ ->  undefined :: ()) 1)
 = print          (undefined :: ())
 = putStrLn (show (undefined :: ()))

y con

instance Show () where
    show :: () -> String
    show x = case x of () -> "()"

Continúa

 = putStrLn (case (undefined :: ()) of () -> "()")

Ahora, la sección 3.17.3 Semántica formal de coincidencia de patrones del informe dice

La semántica de las caseexpresiones [se da] en las Figuras 3.1-3.3. Cualquier implementación debe comportarse de manera que estas identidades se mantengan [...].

y caso (r)en la Figura 3.2 estados

(r)     case  of { K x1  xn -> e; _ -> e } =  
        where K is a data constructor of arity n 

() es el constructor de datos de arity 0, por lo que es lo mismo que

(r)     case  of { () -> e; _ -> e } =  

y el resultado general de la evaluación es así .

Will Ness
fuente
2
Me gusta tu explicación Es claro y simple.
arrowd
@DanielWagner Tenía en mente el case de Core, y estaba ignorando el agujero enorme. :) He editado para mencionar el Core.
Will Ness
1
¿No estaría el forzamiento show invocada por print? Algo así comoshow x = case x of () -> "()"
user253751
1
Me refiero a caseCore, no a Haskell en sí. Haskell se traduce al Core, que tiene un forzado case, AFAIK. Tienes razón caseen que en Haskell no se está forzando por sí solo. Podría escribir algo en Scheme o ML (si pudiera escribir ML), o pseudocódigo.
Will Ness
1
La respuesta autorizada a todo esto probablemente se encuentre en algún lugar del Informe. Todo lo que sé es que aquí no hay "optimización", y "valor regular" no es un término significativo en este contexto. Lo que sea forzado, forzado. printfuerza tanto como sea necesario para imprimir. no mira el tipo, los tipos se han ido, borrado, para cuando se ejecuta el programa, la subrutina de impresión correcta ya está seleccionada y compilada, según el tipo, en el momento de la compilación; esa subrutina seguirá forzando su valor de entrada a WHNF en tiempo de ejecución, y si no está definida, causará un error.
Will Ness