haskell - ¿Cómo funciona ArrowLoop? Además, mfix?
monads arrows (1)
En este código, la pieza clave es la flecha de delay 0
en el bloque de rec
. Para ver cómo funciona, es útil pensar que los valores varían a lo largo del tiempo y se cortan en rebanadas. Pienso en las rebanadas como ''días''. El bloque de rec
explica cómo funcionan los cálculos de cada día. Está organizado por valor , en lugar de por orden causal , pero aún podemos rastrear la causalidad si tenemos cuidado. De manera crucial, debemos asegurarnos (sin ninguna ayuda de los tipos ) que el trabajo de cada día se base en el pasado pero no en el futuro. La delay 0
un día delay 0
nos da tiempo a este respecto: cambia su señal de entrada un día después, cuidando el primer día al dar el valor 0. La señal de entrada de la demora es "la next
de mañana".
rec output <- returnA -< if reset then 0 else next
next <- delay 0 -< output+1
Entonces, mirando las flechas y sus salidas, estamos entregando la output
de hoy, pero la next
mañana . En cuanto a las entradas, confiamos en el reset
de hoy y los valores next
. Está claro que podemos entregar esas salidas de esas entradas sin viajar en el tiempo. La output
es el next
número de hoy a menos que reset
a 0; Mañana, el next
número es el sucesor de la output
de hoy. El next
valor de hoy viene de ayer, a menos que no haya ayer, en cuyo caso es 0.
En un nivel inferior, toda esta configuración funciona debido a la pereza de Haskell. Haskell computa mediante una estrategia basada en la demanda, por lo que si hay un orden secuencial de tareas que respete la causalidad, Haskell la encontrará. Aquí, el delay
establece tal orden.
Sin embargo, tenga en cuenta que el sistema de tipos de Haskell le brinda muy poca ayuda para garantizar que exista tal orden. ¡Eres libre de usar bucles por completo disparate! Así que tu pregunta está lejos de ser trivial. Cada vez que lea o escriba un programa de este tipo, deberá pensar ''¿cómo puede funcionar esto?''. Debe verificar que el delay
(o similar) se use de manera adecuada para garantizar que la información se solicite solo cuando se puede calcular. Tenga en cuenta que los constructores , especialmente (:)
pueden actuar como retrasos: no es inusual calcular la cola de una lista, aparentemente dada la lista completa (pero teniendo cuidado solo de inspeccionar la cabeza). A diferencia de la programación imperativa, el estilo funcional perezoso le permite organizar su código en torno a conceptos distintos a la secuencia de eventos, pero es una libertad que exige una conciencia más sutil del tiempo.
Ahora estoy bastante cómodo con el resto de la maquinaria de flecha, pero no entiendo cómo funciona el bucle. Me parece mágico, y eso es malo para mi comprensión. También tengo problemas para entender mfix. Cuando miro un fragmento de código que usa rec
en un proc
o bloque, me confundo. Con el código monádico o de flecha regular, puedo pasar por el cálculo y mantener una imagen operativa de lo que está pasando en mi cabeza. Cuando llego a la rec
, no sé qué imagen guardar! Me quedo atascado, y no puedo razonar sobre tal código.
El ejemplo que estoy tratando de asimilar es el artículo de Ross Paterson sobre las flechas , el de los circuitos.
counter :: ArrowCircuit a => a Bool Int
counter = proc reset -> do
rec output <- returnA -< if reset then 0 else next
next <- delay 0 -< output+1
returnA -< output
Supongo que si entiendo este ejemplo, podré entender el bucle en general, y será un gran camino para entender mfix. Sienten esencialmente lo mismo para mí, pero tal vez hay una sutileza que me falta. De todos modos, lo que realmente apreciaría es una imagen operativa de tales piezas de código, por lo que puedo recorrerlas en mi cabeza como un código ''regular''.
Edit : Gracias a la respuesta de Pigworker, comencé a pensar en la recreación y en las demandas que se cumplen. Tomando el ejemplo del counter
, la primera línea del bloque de grabación exige un valor llamado output
. Imagino esto operativamente como crear un cuadro, etiquetarlo como output
y pedirle al bloque de grabación que llene ese cuadro. Para rellenar ese cuadro, introducimos un valor en returnA, pero ese valor en sí mismo exige otro valor, denominado a next
. Para poder utilizar este valor, se debe exigir de otra línea en el bloque de grabación, pero no importa en qué parte del bloque de grabación se exige, por ahora .
Así que vamos a la siguiente línea, y encontramos el cuadro etiquetado a next
, y exigimos que otro cómputo lo llene. Ahora, esta computación exige nuestra primera caja! Así que le damos la caja , pero no tiene ningún valor dentro, así que si este cálculo exige el contenido de la output
, llegamos a un bucle infinito. Afortunadamente, la demora toma la caja, pero produce un valor sin mirar dentro de la caja. Esto se llena a next
, que luego nos permite llenar la output
. Ahora que se llena la output
, cuando se procesa la siguiente entrada de este circuito, la caja de output
anterior tendrá su valor, lista para ser demandada para producir la siguiente next
, y por lo tanto la siguiente output
.
¿Como suena eso?