Las fold
diferencias parecen ser una fuente frecuente de confusión, así que aquí hay una descripción más general:
Considere doblar una lista de n valores [x1, x2, x3, x4 ... xn ]
con alguna función f
y semilla z
.
foldl
es:
- Izquierda asociativa :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Cola recursiva : itera a través de la lista, produciendo el valor después
- Perezoso : nada se evalúa hasta que se necesita el resultado
- Al revés :
foldl (flip (:)) []
invierte una lista.
foldr
es:
- Derecho asociativo :
f x1 (f x2 (f x3 (f x4 ... (f xn z) ... )))
- Recursivo en una discusión : cada iteración se aplica
f
al siguiente valor y al resultado de plegar el resto de la lista.
- Perezoso : nada se evalúa hasta que se necesita el resultado
- Adelante :
foldr (:) []
devuelve una lista sin cambios.
Aquí hay un punto ligeramente sutil que a veces hace tropezar a las personas: porque foldl
es al revés cada aplicación de f
se agrega al exterior del resultado; y debido a que es vago , no se evalúa nada hasta que se requiere el resultado. Esto significa que para calcular cualquier parte del resultado, Haskell primero itera a través de la lista completa construyendo una expresión de aplicaciones de funciones anidadas, luego evalúa la función más externa , evaluando sus argumentos según sea necesario. Si f
siempre usa su primer argumento, esto significa que Haskell tiene que recurrir hasta el término más interno, luego trabajar hacia atrás calculando cada aplicación def
.
Obviamente, esto está muy lejos de la eficiente recursión de cola que la mayoría de los programadores funcionales conocen y aman.
De hecho, a pesar de que foldl
es técnicamente recursivo, porque toda la expresión del resultado se construye antes de evaluar cualquier cosa,foldl
puede causar un desbordamiento de la pila!
Por otro lado, considere foldr
. También es vago, pero debido a que se ejecuta hacia adelante , cada aplicación de f
se agrega al interior del resultado. Entonces, para calcular el resultado, Haskell construye una aplicación de función única , cuyo segundo argumento es el resto de la lista plegada. Si f
es flojo en su segundo argumento, un constructor de datos, por ejemplo, el resultado será incrementalmente flojo , y cada paso del pliegue se computará solo cuando se evalúe una parte del resultado que lo necesita.
Entonces, podemos ver por qué a foldr
veces funciona en listas infinitas cuando foldl
no lo hace: la primera puede convertir perezosamente una lista infinita en otra estructura de datos infinita perezosa, mientras que la segunda debe inspeccionar toda la lista para generar cualquier parte del resultado. Por otro lado, foldr
con una función que necesita ambos argumentos de forma inmediata, como (+)
, funciona (o más bien, no funciona) de forma muy similar a foldl
construir una gran expresión antes de evaluarla.
Entonces, los dos puntos importantes a tener en cuenta son estos:
foldr
puede transformar una estructura de datos recursiva perezosa en otra.
- De lo contrario, los pliegues perezosos se bloquearán con un desbordamiento de pila en listas grandes o infinitas.
Es posible que haya notado que parece que foldr
puede hacer todo lo foldl
posible, y más. ¡Esto es verdad! De hecho, ¡ foldl es casi inútil!
Pero, ¿qué sucede si queremos producir un resultado no perezoso al plegar una lista grande (pero no infinita)? Para esto, queremos un pliegue estricto , que las bibliotecas estándar proporcionan cuidadosamente :
foldl'
es:
- Izquierda asociativa :
f ( ... (f (f (f (f z x1) x2) x3) x4) ...) xn
- Cola recursiva : itera a través de la lista, produciendo el valor después
- Estricto : cada aplicación de función se evalúa a lo largo del camino
- Al revés :
foldl' (flip (:)) []
invierte una lista.
Debido a que foldl'
es estricto , para calcular el resultado, Haskell evaluará f
en cada paso, en lugar de dejar que el argumento izquierdo acumule una expresión enorme y sin evaluar. ¡Esto nos da la recurrencia de cola usual y eficiente que queremos! En otras palabras:
foldl'
puede plegar listas grandes de manera eficiente.
foldl'
se colgará en un bucle infinito (no causará un desbordamiento de pila) en una lista infinita.
El wiki de Haskell también tiene una página que discute esto .
foldr
es mejor quefoldl
en Haskell , mientras que lo contrario es cierto en Erlang (que aprendí antes de Haskell ). Como Erlang no es perezoso y las funciones no son curry ,foldl
en Erlang se comporta comofoldl'
anteriormente. ¡Esta es una respuesta genial! Buen trabajo y gracias!