122 votes

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

Par quel mécanisme cette fonction fibonacci est-elle mémoisé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)                    
 

98voto

Will Ness Points 18581

Le mécanisme d'évaluation en Haskell est par nécessité: quand une valeur est nécessaire, il est calculé, et mis à disposition dans le cas où il est demandé à nouveau. Si nous définissons quelques liste, xs=[0..] et, plus tard, de demander pour son 100e élément, xs!!99, le 100e fente dans la liste devient "chair", en maintenant le nombre 99 maintenant, prêt pour le prochain accès.

C'est quoi ce truc, "going-par-une-liste", est en train d'exploiter. Normal doublement recursve Fibonacci définition, fib n = fib (n-1) + fib (n-2), la fonction elle-même est appelée, deux fois par le haut, à l'origine de la hausse exponentielle. Mais avec cette astuce, nous avons établi une liste de résultats intermédiaires, et d'aller "dans la liste":

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

Le truc, c'est à cause de cette liste pour obtenir créé, et faire en sorte que la liste ne pas aller loin (par le biais de la collecte des ordures) entre les appels à l' fib. La façon la plus simple de faire cela est de nom de cette liste. "Si vous avez un nom, il va rester."


Votre première version définit un monomorphe constante, et la deuxième définit une fonction polymorphe. Une fonction polymorphe ne pouvez pas utiliser la même liste interne pour les différents types dont elle a besoin pour servir, donc pas de partage, c'est à dire pas de memoization.

Avec la première version, le compilateur est généreux avec nous, en prenant la constante sous-expression (map fib' [0..]) et en faire un distinct partageable entité, mais il n'est pas soumis à aucune obligation de le faire. et il y a effectivement des cas où on ne souhaitez faire que pour nous automatiquement.

(edit:) tenir compte de ces ré-écrit:

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)

Donc la vraie histoire semble être sur le sous-champ d'application définitions. Il est dénuée de portée avec le 1er et le 3ème est prudent de ne pas appeler de l'extérieur-champ fib3, mais le même niveau f.

Chaque nouvelle invocation d' fib2 semble créer son imbriqués les définitions de nouveau parce que l'un d'eux peut (en théorie) être défini différemment en fonction de la valeur de n (merci à Guy et Tikhon pour le pointage). Avec la première définition il n'y a pas d' n dépendent, et à la troisième il y a une dépendance, mais chaque appel à l' fib3 d'appels en f qui est prudent de n'appeler que les définitions de même au niveau de la portée, à l'intérieur de cette invocation de l' fib3, de sorte que le même xs obtient réutilisés (c'est à dire partagée) pour que l'invocation de l' 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épendants de l'extérieur, n obligatoire, à effectuer le lambda de levage après tout, résultant en plein memoization (sauf pour le polymorphe définitions). En fait, c'est exactement ce qui se passe avec les trois versions déclaré avec monomorphe types et compilé avec-O2 drapeau. Avec polymorphes déclarations de type, fib3 expositions locales, le partage et l' fib2 sans partage à tous.

En fin de compte, selon un compilateur, et les optimisations du compilateur utilisé, et comment vous le tester (le chargement de fichiers dans GHCI, compilé ou non, avec -O2 ou pas, ou autonome), et si elle obtient un monomorphe ou polymorphe type de comportement pourrait changer complètement de savoir si c'expositions locales (par appel), le partage (c'est à dire le temps linéaire sur chaque appel), memoization (c'est à dire le temps linéaire sur première convocation, et 0 dans le temps sur les appels suivants avec la même ou plus petit argument), ou pas de partage à tous (temps exponentiel).

Bref, c'est un compilateur chose. :)

24voto

Tikhon Jelvis Points 30789

Je ne suis pas tout à fait certain, mais en voici une conjecture instruite:

Le compilateur suppose qu' fib n pourrait être différent sur un autre n , et, donc, besoin de recalculer la liste à chaque fois. Les bits à l'intérieur de l' where déclaration pourrait dépendre n, après tout. C'est, dans ce cas, l'ensemble de la liste des numéros est essentiellement une fonction d' n.

La version sans n pouvez créer la liste une fois et l'envelopper dans une fonction. La liste ne peut pas dépendre de la valeur de n passé et c'est facile à vérifier. La liste est une constante qui est ensuite indexé dans. Il est, bien sûr, une constante qui est paresseusement évalué, de sorte que votre programme n'essaie pas d'obtenir l'ensemble (infini) liste immédiatement. Puisque c'est une constante, il peut être partagé entre les appels de fonction.

C'est memoized à tous, parce que l'appel récursif suffit de rechercher une valeur dans une liste. Depuis l' fib version crée la liste une fois que paresseusement, c'est juste calcule assez pour obtenir la réponse sans faire de calcul redondant. Ici, "paresseux" signifie que chaque entrée de la liste est un thunk (non évalué l'expression). Lorsque vous faire évaluer le thunk, il devient une valeur, afin d'accéder à la prochaine fois ne pas répéter le calcul. Depuis la liste peut être partagée entre les appels, toutes les entrées sont déjà calculés par le temps, vous avez besoin prochaine.

Il s'agit essentiellement d'un savant et à faible loyer forme de programmation dynamique basée sur GHC paresseux de la sémantique. Je pense que la norme ne spécifie qu'il doit être non-stricte, de sorte que la conformité de compilateur pourrait potentiellement compiler ce code pour ne pas memoize. Cependant, dans la pratique, tout ce qui est raisonnablement compilateur va être paresseux.

Pour plus d'informations sur pourquoi le second cas, fonctionne à tous, lu à la Compréhension d'une liste définie de manière récursive (bobards en termes de zipWith).

20voto

Daniel Fischer Points 114146

Tout d'abord, avec ghc-7.4.2, compilé avec -O2, le non-memoised version n'est pas si mal, la liste interne des nombres de Fibonacci est encore memoised pour chaque haut-niveau de l'appel à la fonction. Mais il n'est pas, et ne peut, raisonnablement, être memoised à travers les différents haut-niveau des appels. Toutefois, pour l'autre version, la liste est partagée entre les appels.

Cela est dû à la monomorphism restriction.

Le premier est lié par un simple motif de liaison (seulement le nom, sans arguments), et donc par la monomorphism restriction, on doit obtenir un type monomorphe. Le type inféré est

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

et cette contrainte devient par défaut (en l'absence de déclaration par défaut disent le contraire) Integer, la fixation du type

fib :: Int -> Integer

Ainsi, il est juste une liste (de type [Integer]) à memoise.

La seconde est définie avec un argument de fonction, donc il reste polymorphes, et si ce sont des listes de memoised à travers des appels, une liste devrait être memoised pour chaque type, en Num. Ce n'est pas pratique.

Compiler les deux versions avec le monomorphism restriction désactivé, ou avec le même type de signatures, et les deux présentent exactement le même comportement. (Ce n'était pas vrai pour les anciennes versions de compilateur, je ne sais pas de quelle version il l'a fait.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X