¿Mediante qué mecanismo se memoriza esta función de fibonacci?
fib = (map fib' [0..] !!)
where fib' 1 = 1
fib' 2 = 1
fib' n = fib (n-2) + fib (n-1)
Y en una nota relacionada, ¿por qué esta versión no?
fib n = (map fib' [0..] !! n)
where fib' 1 = 1
fib' 2 = 1
fib' n = fib (n-2) + fib (n-1)
fib 0
no termina: probablemente quieras que los casos basefib'
seanfib' 0 = 0
yfib' 1 = 1
.fibs = 1:1:zipWith (+) fibs (tail fibs)
yfib = (fibs !!)
.Respuestas:
El mecanismo de evaluación en Haskell es por necesidad : cuando se necesita un valor, se calcula y se mantiene listo en caso de que se vuelva a solicitar. Si definimos alguna lista,
xs=[0..]
y luego preguntamos por su elemento número 100xs!!99
, el espacio número 100 de la lista se "completa", manteniendo el número99
ahora, listo para el siguiente acceso.Eso es lo que está explotando ese truco de "revisar una lista". En la definición normal de Fibonacci doblemente recurrente
fib n = fib (n-1) + fib (n-2)
, la función en sí se llama, dos veces desde arriba, lo que provoca la explosión exponencial. Pero con ese truco, establecemos una lista para los resultados provisionales y revisamos "la lista":El truco es hacer que se cree esa lista y hacer que esa lista no desaparezca (mediante la recolección de basura) entre llamadas a
fib
. La forma más sencilla de lograrlo es nombrar esa lista. "Si lo nombras, se quedará".Su primera versión define una constante monomórfica y la segunda define una función polimórfica. Una función polimórfica no puede usar la misma lista interna para diferentes tipos que podría necesitar servir, por lo que no se comparte , es decir, no se memoriza.
Con la primera versión, el compilador está siendo generoso con nosotros, eliminando esa subexpresión constante (
map fib' [0..]
) y convirtiéndola en una entidad que se puede compartir por separado, pero no tiene ninguna obligación de hacerlo. y hay casos en los que no queremos que lo haga por nosotros automáticamente.( editar :) Considere estas reescrituras:
Entonces, la verdadera historia parece ser sobre las definiciones de alcance anidadas. No hay un alcance externo con la primera definición, y la tercera tiene cuidado de no llamar al alcance externo
fib3
, sino al mismo nivelf
.Cada nueva invocación de
fib2
parece crear sus definiciones anidadas de nuevo porque cualquiera de ellas podría (en teoría) definirse de manera diferente dependiendo del valor den
(gracias a Vitus y Tikhon por señalar eso). Con la primera definición no hay den
qué depender, y con la tercera hay una dependencia, pero cada llamada separada afib3
llamadas a lasf
que se tiene cuidado de llamar solo definiciones del alcance del mismo nivel, interno a esta invocación específica defib3
, por lo que lo mismo sexs
obtiene reutilizado (es decir, compartido) para esa invocación defib3
.Pero nada impide que el compilador reconozca que las definiciones internas en cualquiera de las versiones anteriores son de hecho independientes del
n
enlace externo , para realizar el levantamiento lambda después de todo, lo que resulta en una memorización completa (a excepción de las definiciones polimórficas). De hecho, eso es exactamente lo que sucede con las tres versiones cuando se declaran con tipos monomórficos y se compilan con el indicador -O2. Con declaraciones de tipo polimórfico,fib3
exhibe compartición local yfib2
no compartición en absoluto.En última instancia, según el compilador y las optimizaciones del compilador utilizadas, y cómo lo prueba (cargando archivos en GHCI, compilados o no, con -O2 o no, o independiente), y si obtiene un tipo monomórfico o polimórfico, el comportamiento podría cambiar por completo, ya sea que muestre uso compartido local (por llamada) (es decir, tiempo lineal en cada llamada), memorización (es decir, tiempo lineal en la primera llamada y tiempo 0 en llamadas posteriores con el mismo argumento o un argumento menor), o sin compartir en absoluto ( tiempo exponencial).
La respuesta corta es que es cosa de compiladores. :)
fuente
fib'
se redefine para todosn
y, por lo tanto,fib'
enfib 1
≠fib'
infib 2
, lo que también implica que las listas son diferentes. Incluso si corrige el tipo para que sea monomórfico, todavía presenta este comportamiento.where
las cláusulas introducen el intercambio delet
expresiones muy parecidas , pero tienden a ocultar problemas como éste. Reescribiéndolo un poco más explícitamente, obtienes esto: hpaste.org/71406Int -> Integer
), luego sefib2
ejecuta en tiempo exponencial,fib1
yfib3
ambos se ejecutan en tiempo lineal perofib1
también se memorizan, nuevamente porquefib3
las definiciones locales se redefinen para todosn
.pwr (x:xs) = pwr xs ++ map (x:) pwr xs ; pwr [] = [[]]
queremospwr xs
que se calcule de forma independiente, dos veces, para que se pueda recolectar la basura sobre la marcha a medida que se produce y consume.No estoy del todo seguro, pero he aquí una suposición fundamentada:
El compilador asume que
fib n
podría ser diferente en una diferenten
y por lo tanto necesitaría recalcular la lista cada vez. Después de todo, los bits dentro de lawhere
declaración podrían dependern
. Es decir, en este caso, la lista completa de números es esencialmente una función den
.La versión sin
n
puede crear la lista una vez y envolverla en una función. La lista no puede depender del valor den
pasado y esto es fácil de verificar. La lista es una constante que luego se indexa. Es, por supuesto, una constante que se evalúa de forma perezosa, por lo que su programa no intenta obtener la lista completa (infinita) inmediatamente. Dado que es una constante, se puede compartir entre las llamadas a funciones.Se memoriza en absoluto porque la llamada recursiva solo tiene que buscar un valor en una lista. Dado que la
fib
versión crea la lista una vez de forma perezosa, solo calcula lo suficiente para obtener la respuesta sin hacer cálculos redundantes. Aquí, "lazy" significa que cada entrada en la lista es un procesador (una expresión no evaluada). Al no evaluar el golpe seco, se convierte en un valor, por lo que el acceso a la próxima vez no sin repetir el cálculo. Dado que la lista se puede compartir entre llamadas, todas las entradas anteriores ya están calculadas para cuando necesite la siguiente.Es esencialmente una forma inteligente y de bajo costo de programación dinámica basada en la semántica perezosa de GHC. Creo que el estándar solo especifica que tiene que ser no estricto , por lo que un compilador compatible podría compilar este código para no memorizar. Sin embargo, en la práctica, todo compilador razonable será vago.
Para obtener más información sobre por qué el segundo caso funciona, lea Comprensión de una lista definida de forma recursiva (fibs en términos de zipWith) .
fuente
fib' n
podría ser diferente en un diferenten
" quizás?fib
, incluidofib'
, puede ser diferente en todos los aspectosn
. Creo que el ejemplo original es un poco confuso porquefib'
también depende de sí mismo lon
que ensombrece al otron
.Primero, con ghc-7.4.2, compilado con
-O2
, la versión no memorizada no es tan mala, la lista interna de números de Fibonacci todavía se memoriza para cada llamada de nivel superior a la función. Pero no se puede memorizar, ni razonablemente, en las diferentes llamadas de alto nivel. Sin embargo, para la otra versión, la lista se comparte entre llamadas.Eso se debe a la restricción del monomorfismo.
El primero está limitado por un patrón de enlace simple (solo el nombre, sin argumentos), por lo tanto, por la restricción de monomorfismo debe obtener un tipo monomorfo. El tipo inferido es
y dicha restricción se establece por defecto (en ausencia de una declaración predeterminada que diga lo contrario) a
Integer
, fijando el tipo comoPor lo tanto, solo hay una lista (de tipo
[Integer]
) para memorizar.El segundo se define con un argumento de función, por lo que sigue siendo polimórfico, y si las listas internas se memorizaran a través de las llamadas, se tendría que memorizar una lista para cada tipo
Num
. Eso no es práctico.Compile ambas versiones con la restricción de monomorfismo desactivada o con firmas de tipo idénticas, y ambas exhiben exactamente el mismo comportamiento. (Eso no fue cierto para las versiones anteriores del compilador, no sé qué versión lo hizo primero).
fuente
fib 1000000
a muchos tipos, eso consume una tonelada de memoria. Para evitar eso, se necesitaría una heurística que arroje listas de la caché cuando crezca demasiado. Y tal estrategia de memorización también se aplicaría a otras funciones o valores, presumiblemente, por lo que el compilador tendría que lidiar con un número potencialmente grande de cosas para memorizar potencialmente para muchos tipos. Creo que sería posible implementar la memorización polimórfica (parcial) con una heurística razonablemente buena, pero dudo que valga la pena.No necesita la función de memoria para Haskell. Solo el lenguaje de programación empirativo necesita esas funciones. Sin embargo, Haskel es lenguaje funcional y ...
Entonces, este es un ejemplo de algoritmo de Fibonacci muy rápido:
zipWith es una función de Prelude estándar:
Prueba:
Salida:
Tiempo transcurrido: 0.00018s
fuente