J'ai ce "code d'apprentissage" que j'ai écrit pour le morris seq dans f # qui souffre d'un débordement de pile que je ne sais pas comment éviter. "morris" renvoie une séquence infinie de séquences "voir et dire" (ie, {{1}, {1,1}, {2,1}, {1,2,1,1}, {1,1,1 2,2,1}, {3,1,2,2,1,1}, ...}).Éviter le débordement de pile (avec des séquences infinies de séquences F #)
let printList l =
Seq.iter (fun n -> printf "%i" n) l
printfn ""
let rec morris s =
let next str = seq {
let cnt = ref 1 // Stack overflow is below when enumerating
for cur in [|0|] |> Seq.append str |> Seq.windowed 2 do
if cur.[0] <> cur.[1] then
yield!([!cnt ; cur.[0]])
cnt := 0
incr cnt
}
seq {
yield s
yield! morris (next s) // tail recursion, no stack overflow
}
// "main"
// Print the nth iteration
let _ = [1] |> morris |> Seq.nth 3125 |> printList
Vous pouvez sélectionner la ième itération à l'aide de Seq.nth, mais vous ne pouvez vous en rendre si loin avant d'avoir atteint un débordement de pile. Le seul élément de récursion que j'ai est la récursion de la queue, qui constitue essentiellement un ensemble d'énumérateurs liés. Ce n'est pas où le problème est. C'est quand "enum" est appelé sur le dire la 4000e séquence. Notez que c'est avec F # 1.9.6.16, la version précédente a dépassé 14000). C'est parce que la façon dont les séquences liées sont résolues. Les séquences sont paresseuses et donc la "récursion" est paresseuse. C'est-à-dire, seq n appelle seq n-1 qui appelle seq n-2 et ainsi de suite pour obtenir le premier élément (le tout premier # est le pire des cas). Je comprends que [|0|] |> Seq.append str |> Seq.windowed 2
, aggrave mon problème et je pourrais tripler le # que je pourrais générer si je l'éliminais. En pratique, le code fonctionne assez bien. La 3125ème itération de Morris aurait plus de 10^359 caractères de longueur.
Le problème que j'essaie vraiment de résoudre est de savoir comment conserver l'eval paresseux et ne pas avoir de limite en fonction de la taille de la pile pour l'itération que je peux détecter. Je suis à la recherche de l'idiome F # approprié pour faire la limite basée sur la taille de la mémoire.
Mise à jour octobre '10
Après avoir appris F # un peu mieux, un petit peu de Haskell, en pensant & enquêter sur ce problème depuis plus d'année, je peux enfin répondre à ma propre question. Mais comme toujours avec des problèmes difficiles, le problème commence avec la mauvaise question. Le problème n'est pas des séquences de séquences - c'est vraiment à cause d'une séquence récursivement définie. Mes compétences en programmation fonctionnelle sont un peu mieux maintenant et il est donc plus facile de voir ce qui se passe avec la version ci-dessous, qui obtient encore un stackoverflow
let next str =
Seq.append str [0]
|> Seq.pairwise
|> Seq.scan (fun (n,_) (c,v) ->
if (c = v) then (n+1,Seq.empty)
else (1,Seq.ofList [n;c])) (1,Seq.empty)
|> Seq.collect snd
let morris = Seq.unfold(fun sq -> Some(sq,next sq))
Cela crée basicially une chaîne très longue de la fonction de traitement Seq appels pour générer la les suites. Le module Seq fourni avec F # est ce qui ne peut pas suivre la chaîne sans utiliser la pile. Il y a une optimisation qu'il utilise pour les séquences append et récursivement définies, mais cette optimisation ne fonctionne que si la récursion implémente un append.
donc ça marchera
let rec ints n = seq { yield n; yield! ints (n+1) }
printf "%A" (ints 0 |> Seq.nth 100000);;
Et celui-ci aura une stackoverflow.
let rec ints n = seq { yield n; yield! (ints (n+1)|> Seq.map id) }
printf "%A" (ints 0 |> Seq.nth 100000);;
Pour prouver le F # libary était la question, je l'ai écrit mon propre module Seq qui a mis append, par paires, de numérisation et de recueillir l'aide continutions et maintenant je peux commencer à générer et imprimer les 50 000 suivants sans problème (il ne finira jamais car il fait plus de 10^5697 chiffres).
Quelques notes supplémentaires:
- continuations étaient l'idiome que je cherchais, mais dans ce cas, ils ont dû aller dans la bibliothèque F #, mon code. J'ai appris des suites dans F # de Tomas Petricek'sReal-World Functional Programming livre.
- La réponse liste paresseuse que j'ai acceptée contenait l'autre idiome; évaluation paresseuse. Dans ma bibliothèque réécrite, j'ai également dû tirer parti du type paresseux pour éviter le stackoverflow. La version de liste paresseuse sorta des œuvres par chance (peut-être par conception mais cela dépasse ma capacité actuelle à déterminer) - la correspondance de pattern active utilisée pendant sa construction et son itération amène les listes à calculer des valeurs avant que la récursivité requise profond, donc c'est paresseux, mais pas si paresseux il a besoin de continuations pour éviter le stackoverflow. Par exemple, au moment où la 2e séquence a besoin d'un chiffre de la 1ère séquence, elle a déjà été calculée. En d'autres termes, la version LL n'est pas strictement JIT paresseux pour la génération de séquence, seulement la gestion des listes.
De combien de temps votre algorithme a-t-il besoin pour calculer le 60ème élément de Morris? – Dario
Je ne connais pas l'heure exacte. C'est probablement 4 minutes plus. La version C++ que l'un de mes collègues a fait est sub second. Le plus fonctionnel, je le fais le plus lent qu'il obtient. C'est toute la création d'objet. La version ci-dessus commence à créer une sortie tout de suite, même à 14000. –
Cette version n'est pas tout à fait fonctionnelle de toute façon. J'ai écrit ceci dans Haskell d'une manière purement fonctionnelle qui est a) beaucoup plus concise (seulement les listes + correspondance de modèle) et b) encore plus rapide ;-) – Dario