web-dev-qa-db-fra.com

Comment cette fonction fibonacci est-elle mémorisée?

Par quel mécanisme cette fonction fibonacci est-elle mémorisée?

fib = (map fib' [0..] !!)                 
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    

Et sur une note connexe, pourquoi cette version n'est-elle pas?

fib n = (map fib' [0..] !! n)                                               
     where fib' 1 = 1                                                        
           fib' 2 = 1                                                        
           fib' n = fib (n-2) + fib (n-1)                    
112
bjornars

Le mécanisme d'évaluation dans Haskell est par besoin: lorsqu'une valeur est nécessaire, elle est calculée et maintenue prête au cas où elle serait demandée à nouveau. Si nous définissons une liste, xs=[0..] Et demandons plus tard son 100e élément, xs!!99, Le 100e emplacement de la liste est "étoffé", en maintenant le numéro 99, prêt pour le prochain accès.

C'est ce que cette astuce, "parcourir une liste", exploite. Dans la définition normale de Fibonacci doublement récurrente, fib n = fib (n-1) + fib (n-2), la fonction elle-même est appelée, deux fois par le haut, provoquant l'explosion exponentielle. Mais avec cette astuce, nous avons établi une liste pour les résultats intermédiaires et parcourons "la liste":

fib n = (xs!!(n-1)) + (xs!!(n-2)) where xs = 0:1:map fib [2..]

L'astuce consiste à créer cette liste et à ne pas la faire disparaître (par le biais de la récupération de place) entre les appels à fib. La manière la plus simple d'y parvenir est de name cette liste. "Si vous le nommez, il restera."


Votre première version définit une constante monomorphe, et la seconde définit une fonction polymorphe. Une fonction polymorphe ne peut pas utiliser la même liste interne pour différents types qu'elle pourrait avoir à servir, donc pas de partage, c'est-à-dire pas de mémorisation.

Avec la première version, le compilateur est généreux avec nous, supprimant cette sous-expression constante (map fib' [0..]) Et en faisant une entité partageable séparée, mais il n'est pas obligé de le faire alors. et il y a en fait des cas où nous ne le faites pas voulons qu'il le fasse automatiquement pour nous.

( edit: ) Considérez ces réécritures:

fib1 = f                     fib2 n = f n                 fib3 n = f n          
 where                        where                        where                
  f i = xs !! i                f i = xs !! i                f i = xs !! i       
  xs = map fib' [0..]          xs = map fib' [0..]          xs = map fib' [0..] 
  fib' 1 = 1                   fib' 1 = 1                   fib' 1 = 1          
  fib' 2 = 1                   fib' 2 = 1                   fib' 2 = 1          
  fib' i=fib1(i-2)+fib1(i-1)   fib' i=fib2(i-2)+fib2(i-1)   fib' i=f(i-2)+f(i-1)

La vraie histoire semble donc concerner les définitions de portée imbriquées. Il n'y a pas de portée externe avec la 1ère définition, et la 3ème prend soin de ne pas appeler la portée externe fib3, Mais le même niveau f.

Chaque nouvelle invocation de fib2 Semble recréer ses définitions imbriquées car l'une d'entre elles pourrait (en théorie) être définie différemment selon sur le valeur de n (merci à Vitus et Tikhon de l'avoir signalé). Avec la première définition, il n'y a pas de n sur lequel s'appuyer, et avec la troisième il y a une dépendance, mais chaque appel distinct à fib3 Appelle dans f qui prend soin d'appeler uniquement définitions de portée de même niveau, internes à cette invocation spécifique de fib3, donc le même xs est réutilisé (c'est-à-dire partagé) pour cette invocation de fib3.

Mais rien n'empêche le compilateur de reconnaître que les définitions internes dans l'une des versions ci-dessus sont en fait indépendant de la liaison externe n, pour effectuer le lambda lifting après tout, résultant en une mémorisation complète (sauf pour les définitions polymorphes). En fait, c'est exactement ce qui se passe avec les trois versions lorsqu'elles sont déclarées avec des types monomorphes et compilées avec l'indicateur -O2. Avec les déclarations de type polymorphe, fib3 Présente un partage local et fib2 Aucun partage du tout.

En fin de compte, en fonction d'un compilateur et des optimisations de compilateur utilisées, et de la façon dont vous le testez (chargement de fichiers dans GHCI, compilés ou non, avec -O2 ou non, ou autonome), et s'il obtient un type monomorphe ou polymorphe, le comportement pourrait changer complètement - qu'il présente un partage local (par appel) (c'est-à-dire un temps linéaire sur chaque appel), une mémorisation (c'est-à-dire un temps linéaire sur le premier appel et 0 fois sur les appels suivants avec le même argument ou un argument plus petit), ou aucun partage du tout ( temps exponentiel).

La réponse courte est, c'est une chose de compilateur. :)

93
Will Ness

Je ne suis pas entièrement certain, mais voici une supposition éclairée:

Le compilateur suppose que fib n pourrait être différent sur un n différent et devrait donc recalculer la liste à chaque fois. Les bits à l'intérieur de l'instruction where pourraient dépendre de n, après tout. Autrement dit, dans ce cas, la liste entière des nombres est essentiellement une fonction de n.

La version sans n peut créer la liste une fois et l'encapsuler dans une fonction. La liste ne peut pas dépendre de la valeur de n transmise et c'est facile à vérifier. La liste est une constante qui est ensuite indexée dans. C'est, bien sûr, une constante qui est évaluée paresseusement, donc votre programme n'essaie pas d'obtenir la liste entière (infinie) immédiatement. Puisqu'il s'agit d'une constante, elle peut être partagée entre les appels de fonction.

Il est mémorisé du tout parce que l'appel récursif n'a qu'à rechercher une valeur dans une liste. Puisque la version fib crée la liste une fois paresseusement, elle calcule juste assez pour obtenir la réponse sans faire de calcul redondant. Ici, "paresseux" signifie que chaque entrée de la liste est un thunk (une expression non évaluée). Lorsque vous évaluez le thunk, il devient une valeur, donc y accéder la prochaine fois ne répétera pas le calcul. Étant donné que la liste peut être partagée entre les appels, toutes les entrées précédentes sont déjà calculées au moment où vous avez besoin de la suivante.

Il s'agit essentiellement d'une forme intelligente et peu coûteuse de programmation dynamique basée sur la sémantique paresseuse de GHC. Je pense que la norme spécifie seulement qu'elle doit être non stricte , donc un compilateur conforme pourrait potentiellement compiler ce code en pas mémoriser. Cependant, dans la pratique, chaque compilateur raisonnable sera paresseux.

Pour plus d'informations sur les raisons pour lesquelles le deuxième cas fonctionne, lisez Comprendre une liste définie récursivement (fibs en termes de zipWith) .

23
Tikhon Jelvis

Tout d'abord, avec ghc-7.4.2, compilé avec -O2, la version non mémorisée n'est pas si mauvaise, la liste interne des numéros de Fibonacci est toujours mémorisée pour chaque appel de niveau supérieur à la fonction. Mais il n'est pas, et ne peut raisonnablement, être mémorisé à travers différents appels de niveau supérieur. Cependant, pour l'autre version, la liste est partagée entre les appels.

Cela est dû à la restriction du monomorphisme.

Le premier est lié par une simple liaison de motif (uniquement le nom, pas d'arguments), donc par la restriction du monomorphisme, il doit obtenir un type monomorphe. Le type déduit est

fib :: (Num n) => Int -> n

et une telle contrainte devient par défaut (en l'absence d'une déclaration par défaut disant le contraire) à Integer, fixant le type comme

fib :: Int -> Integer

Il n'y a donc qu'une seule liste (de type [Integer]) à memoise.

Le second est défini avec un argument de fonction, il reste donc polymorphe, et si les listes internes étaient mémorisées entre les appels, une liste devrait être mémorisée pour chaque type dans Num. Ce n'est pas pratique.

Compilez les deux versions avec la restriction de monomorphisme désactivée, ou avec des signatures de type identiques, et les deux présentent exactement le même comportement. (Ce n'était pas vrai pour les anciennes versions du compilateur, je ne sais pas quelle version l'a fait en premier.)

20
Daniel Fischer

Vous n'avez pas besoin de fonction de mémorisation pour Haskell. Seul le langage de programmation empirique a besoin de ces fonctions. Cependant, Haskel est fonctionnel lang et ...

Voici donc un exemple d'algorithme de Fibonacci très rapide:

fib = zipWith (+) (0:(1:fib)) (1:fib)

zipAvec la fonction du Prélude standard:

zipWith :: (a->b->c) -> [a]->[b]->[c]
zipWith op (n1:val1) (n2:val2) = (n1 + n2) : (zipWith op val1 val2)
zipWith _ _ _ = []

Tester:

print $ take 100 fib

Production:

[1,2,3,5,8,13,21,34,55,89,144,233,377,610,987,1597,2584,4181,6765,10946,17711,28657,46368,75025,121393,196418,317811,514229,832040,1346269,2178309,3524578,5702887,9227465,14930352,24157817,39088169,63245986,102334155,165580141,267914296,433494437,701408733,1134903170,1836311903,2971215073,4807526976,7778742049,12586269025,20365011074,32951280099,53316291173,86267571272,139583862445,225851433717,365435296162,591286729879,956722026041,1548008755920,2504730781961,4052739537881,6557470319842,10610209857723,17167680177565,27777890035288,44945570212853,72723460248141,117669030460994,190392490709135,308061521170129,498454011879264,806515533049393,1304969544928657,2111485077978050,3416454622906707,5527939700884757,8944394323791464,14472334024676221,23416728348467685,37889062373143906,61305790721611591,99194853094755497,160500643816367088,259695496911122585,420196140727489673,679891637638612258,1100087778366101931,1779979416004714189,2880067194370816120,4660046610375530309,7540113804746346429,12200160415121876738,19740274219868223167,31940434634990099905,51680708854858323072,83621143489848422977,135301852344706746049,218922995834555169026,354224848179261915075,573147844013817084101]

Temps écoulé: 0,00018s