Je vais vous expliquer un peu comment cela fonctionne en interne. Tout d'abord, vous devez savoir que Haskell utilise une chose appelée un "objet". jeté pour ses valeurs. Un thunk est fondamentalement une valeur qui n'a pas encore été calculée -- pensez-y comme une fonction à 0 argument. Quand Haskell le souhaite, il peut évaluer (ou partiellement évaluer) le thunk, le transformant en une valeur réelle. S'il ne fait que en partie évalue un thunk, alors la valeur résultante contiendra plus de thunks.
Par exemple, considérez l'expression :
(2 + 3, 4)
Dans un langage ordinaire, cette valeur serait stockée en mémoire sous la forme de (5, 4)
mais en Haskell, il est stocké sous forme de (<thunk 2 + 3>, 4)
. Si vous demandez le deuxième élément de ce tuple, il vous répondra "4", sans jamais additionner 2 et 3. Ce n'est que si vous demandez le premier élément de ce tuple qu'il évaluera le thunk et réalisera que c'est 5.
Avec les fibres, c'est un peu plus compliqué, parce que c'est récursif, mais on peut utiliser la même idée. Parce que fibs
ne prend aucun argument, Haskell stockera de manière permanente tous les éléments de la liste qui ont été découverts -- c'est important.
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
Il permet de visualiser les connaissances actuelles de Haskell sur trois expressions : fibs
, tail fibs
y zipWith (+) fibs (tail fibs)
. Nous supposerons que Haskell commence par connaître les éléments suivants :
fibs = 0 : 1 : <thunk1>
tail fibs = 1 : <thunk1>
zipWith (+) fibs (tail fibs) = <thunk1>
Notez que la deuxième ligne est juste la première décalée vers la gauche, et que la troisième ligne est la somme des deux premières lignes.
Demandez take 2 fibs
et vous obtiendrez [0, 1]
. Haskell n'a pas besoin d'évaluer plus avant ce qui précède pour le savoir.
Demandez take 3 fibs
et Haskell obtiendra le 0 et le 1, puis se rendra compte qu'il doit évaluer en partie le thunk. Afin d'évaluer pleinement zipWith (+) fibs (tail fibs)
il doit additionner les deux premières lignes. Il ne peut pas le faire entièrement, mais il peut commencer pour additionner les deux premières lignes :
fibs = 0 : 1 : 1: <thunk2>
tail fibs = 1 : 1 : <thunk2>
zipWith (+) fibs (tail fibs) = 1 : <thunk2>
Notez que j'ai rempli le "1" dans la troisième ligne, et qu'il est apparu automatiquement dans la première et la deuxième ligne également, puisque les trois lignes partagent le même thunk (pensez-y comme un pointeur qui a été écrit). Et parce qu'il n'a pas fini d'évaluer, il a créé un nouveau thunk contenant le "1". reste de la liste, si cela s'avérait nécessaire.
Mais ce n'est pas nécessaire, car take 3 fibs
est fait : [0, 1, 1]
. Mais maintenant, disons que vous demandez take 50 fibs
Haskell se souvient déjà des 0, 1 et 1. Mais il doit continuer. Il continue donc à additionner les deux premières lignes :
fibs = 0 : 1 : 1 : 2 : <thunk3>
tail fibs = 1 : 1 : 2 : <thunk3>
zipWith (+) fibs (tail fibs) = 1 : 2 : <thunk3>
...
fibs = 0 : 1 : 1 : 2 : 3 : <thunk4>
tail fibs = 1 : 1 : 2 : 3 : <thunk4>
zipWith (+) fibs (tail fibs) = 1 : 2 : 3 : <thunk4>
Et ainsi de suite, jusqu'à ce qu'il ait rempli 48 colonnes de la troisième ligne, et donc calculé les 50 premiers chiffres. Haskell évalue juste ce dont il a besoin, et laisse le "reste" infini de la séquence sous forme d'objet thunk au cas où il en aurait besoin.
Notez que si vous demandez par la suite take 25 fibs
Haskell ne l'évaluera pas à nouveau - il prendra simplement les 25 premiers chiffres de la liste qu'il a déjà calculée.
Modifier : Ajout d'un numéro unique à chaque thunk pour éviter toute confusion.