Éliminer la récursivité - un aperçu de la théorie dans les coulisses

10

Je suis nouveau sur ce site et cette question n'est certainement pas au niveau de la recherche - mais bon. J'ai un peu d'expérience en génie logiciel et presque aucun en CSTheory, mais je le trouve attrayant. Pour faire une longue histoire, je voudrais une réponse plus détaillée à ce qui suit si cette question est acceptable sur ce site.

Donc, je sais que chaque programme récursif a un analogue itératif et je comprends en quelque sorte l'explication populaire qui lui est proposée en conservant quelque chose de similaire à la "pile système" et en poussant les paramètres d'environnement comme l'adresse de retour, etc. Je trouve ce type d'ondes manuelles .

Étant un peu plus concret, je voudrais (formellement) voir comment prouver cette affirmation dans les cas où vous avez une fonction invoquant la chaîne . De plus, que se passe-t-il s'il y a des instructions conditionnelles qui pourraient amener un appeler un certain ? C'est-à-dire que le graphe d'appel de fonction potentiel a des composants fortement connectés.F i F jF0F1FiFi+1FnF0FiFj

Je voudrais savoir comment ces situations peuvent être gérées par disons un convertisseur récursif en itératif. Et la description ondulée à laquelle j'ai fait référence plus haut est-elle vraiment suffisante pour ce problème? Je veux dire alors pourquoi est-ce que je trouve la suppression de la récursivité dans certains cas facile. En particulier, supprimer la récursivité de la traversée en pré-commande d'un arbre binaire est vraiment facile - c'est une question d'entretien standard, mais supprimer la récursivité en cas de post-commande a toujours été un cauchemar pour moi.

Ce que je demande vraiment, c'est questions2

(1) Existe-t-il vraiment une preuve plus formelle (convaincante?) Que la récursivité peut être convertie en itération?

(2) Si cette théorie est vraiment là-bas, alors pourquoi est-ce que je trouve, par exemple, l' itération de la précommande plus facile et la post-commande si difficile? (autre que mon intelligence limitée)

Itachi Uchiha
la source
1
comme le mot itératif :)
Akash Kumar
je ne suis pas sûr de bien comprendre, mais si la récursivité se termine quelque part, vous pouvez réellement simuler une pile système en utilisant votre propre pile. Pour la partie (2), les problèmes ne sont pas différents en termes de complexité de calcul.
singhsumit
Je pense que cette question aurait été le mieux adapté pour le Computer Science site qui est encore en direct. Quant à votre deuxième question, pouvez-vous expliquer pourquoi vous pensez que c'est plus difficile? Le processus devrait être presque identique.
Raphael
merci à tous pour vos commentaires - je suppose que j'ai beaucoup de lecture à faire.
Itachi Uchiha
@Raphael - Un commentaire sur la raison pour laquelle je pense que l'itération de l'ordre postérieur est difficile (en plus de ne pas pouvoir le faire). Je lisais quelques articles sur la suppression de la récursivité et suis tombé sur quelque chose appelé fonctions récursives de queue. Il s'avère qu'ils sont plus faciles à itérer. Je ne comprends toujours pas officiellement pourquoi est-ce vrai; mais il y a autre chose que je dois ajouter. J'ai entendu dire que l'itératisation de la post-commande nécessite deux piles et non une, mais je ne connais pas les détails. Et maintenant je suis perdu - pourquoi cette différence entre ces deux modes de traversée? Et pourquoi la récursivité de la queue est-elle facile à gérer?
Itachi Uchiha

Réponses:

6

Si je comprends bien, vous êtes clair sur la conversion de fonctions qui ne contiennent aucun autre appel de fonction que pour elles-mêmes.

Donc , supposons que nous avons une « chaîne d'appel » . Si nous supposons en outre que ne sont pas eux-mêmes récursifs (car nous les avons déjà convertis), nous pouvons intégrer tous ces appels dans la définition de qui devient ainsi une fonction directement récursive que nous pouvons déjà traiter.FF1FnFF1,,FnF

Cela échoue si certains ont lui-même une chaîne d'appel récursive dans laquelle se produit, c'est-à-dire . Dans ce cas, nous avons une récursion mutuelle qui nécessite une autre astuce pour se débarrasser. L'idée est de calculer les deux fonctions simultanément. Par exemple, dans le cas trivial: F F jF F jFjFFjFFj

f(0) = a
f(n) = f'(g(n-1))

g(0) = b
g(n) = g'(f(n-1))

avec f'et les g'fonctions non-récursifs (ou au moins indépendant de fet g) devient

h(0) = (a,b)
h(n) = let (f,g) = h(n-1) in (f'(g), g'(f)) end

f(n) = let (f, _) = h(n) in f end
g(n) = let (_, g) = h(n) in g end

Cela s'étend naturellement à plus de fonctions impliquées et à des fonctions plus compliquées.

Raphael
la source
Heureux d'avoir pu aider. N'oubliez pas d'accepter votre réponse préférée en cliquant sur la coche à côté.
Raphael
1
Raphel, votre astuce ne fonctionne que lorsque les deux fonctions récursives acceptent des arguments du même type. Si fet gacceptez différents types de types, une astuce plus générale est nécessaire.
Andrej Bauer
@AndrejBauer bonne observation, j'ai totalement raté ça. j'ai vraiment aimé l'approche de raphael, mais comme vous l'avez observé dans les cas généraux, nous avons probablement besoin d'une idée différente. Pouvez-vous faire d'autres suggestions?
Itachi Uchiha
@AndrejBauer True. Je pensais d'un point de vue théorique récursif; ici, nous n'avons que des nombres naturels. C'est suffisant car nous pouvons tout encoder de manière appropriée. Mais votre point est très valable pour la pratique. Je suppose que nous aurions à réécrire fet gjusqu'à ce qu'ils partagent un codage d'entrée et un schéma de récursivité communs (nous ne pouvons pas en avoir un en utilisant le et l'autre ). n1n2
Raphael
Eh bien, voyez ma réponse sur la façon de le faire.
Andrej Bauer
8

Oui, il existe des raisons convaincantes de croire que la récursivité peut être transformée en itération. C'est ce que fait chaque compilateur lorsqu'il traduit le code source en langage machine. Pour la théorie, vous devez suivre les suggestions de Dave Clarke. Si vous souhaitez voir le code réel qui convertit la récursivité en code non récursif, jetez un œil au machine.mllangage MiniML dans mon zoo PL (notez que la loopfonction en bas, qui exécute réellement le code, est récursive et donc elle peut être trivialement converti en une boucle réelle).

Encore une chose. MiniML ne prend pas en charge les fonctions mutuellement récursives. Mais ce n'est pas un problème. Si vous avez une récursion mutuelle entre les fonctions

f1:A1B1
f2:A2B2
fn:AnBn

la récursivité peut être exprimée en termes d'une seule carte récursive

f:A1++AnB1++Bn,
Andrej Bauer
la source
8

Vous voudrez peut-être regarder la machine SECD . Un langage fonctionnel (bien qu'il puisse s'agir de n'importe quel langage) est traduit en une série d'instructions qui gèrent des choses comme mettre des arguments de piles, "invoquer" de nouvelles fonctions et ainsi de suite, le tout géré par une simple boucle.
Les appels récursifs ne sont jamais réellement invoqués. Au lieu de cela, les instructions du corps de la fonction appelée sont placées sur la pile pour s'exécuter.

Une approche connexe est la machine CEK .

Ces deux éléments existent depuis longtemps, il y a donc beaucoup de travail là-dessus. Et bien sûr, il existe des preuves qu'ils fonctionnent et la procédure de "compilation" d'un programme en instructions SECD est linéaire dans la taille du programme (il n'a pas à penser au programme).

Le point de ma réponse est qu'il existe une procédure automatique pour faire ce que vous voulez. Malheureusement, la transformation ne sera pas nécessairement en termes de choses qui sont immédiatement faciles à interpréter pour un programmeur. Je pense que la clé est que lorsque vous voulez itérer un programme, vous devez stocker sur la pile ce que le programme doit faire lorsque vous revenez d'un appel de fonction itéré (c'est ce qu'on appelle une continuation). Pour certaines fonctions (telles que les fonctions récursives de queue), la continuation est triviale. Pour d'autres, la suite peut être très complexe, surtout si vous devez l'encoder vous-même.

Dave Clarke
la source
je serai honnête ici. Je veux vraiment comprendre pourquoi (et comment) pouvez-vous itérer chaque programme récursif. Mais j'ai du mal à lire un document - ils ne me sont généralement pas accessibles. je veux dire que je veux une raison plus profonde que la description "ondulée" dont j'ai parlé dans la question. mais je suis également satisfait de quelque chose qui me donne un nouvel aperçu - cela ne doit pas être la preuve dans ses moindres détails
Itachi Uchiha
[cntd] Je veux dire que j'aimerai la preuve, s'il y en a une, pour me dire pourquoi il est plus facile d'itérer un programme que l'autre. Mais dans un certain sens, le convertisseur récursif en itératif devrait fonctionner quel que soit le programme récursif qu'il prend en entrée. Nt bien sûr, mais je suppose que faire un tel convertisseur pourrait être aussi difficile que le problème d'arrêt? Je suis en train de deviner ici - mais j'aimerais qu'un convertisseur récursif en itératif existe et s'il le fait, je voudrais qu'il explique la complexité inhérente à l'itération de différents programmes récursifs. Je ne suis pas sûr, mais dois-je modifier la question? Ma question est-elle claire?
Itachi Uchiha
@ItachiUchiha - Je ne pense pas que votre problème soit indécidable. Regardez la réponse d'Andrej Bauer. Il note que chaque compilateur le fait lorsqu'il traduit le code source en langage machine. Il ajoute également que vous pouvez voir le code réel qui convertit récursif en non récursif dans le langage MiniM (a) l. Cela indique clairement qu'il existe une procédure de décision pour "itérer" la récursivité. Je ne suis pas sûr de la difficulté / complexité inhérente (conceptuelle) de supprimer la récursivité. Je ne comprends pas très clairement cette question mais elle semble intéressante. Vous pouvez peut-être modifier votre question pour obtenir une meilleure réponse
Akash Kumar
Le point de ma réponse est qu'il existe une procédure automatique pour faire ce que vous voulez. Malheureusement, la transformation ne sera pas nécessairement en termes de choses qui sont immédiatement faciles à interpréter pour un programmeur. Je pense que la clé est que lorsque vous voulez itérer un programme, vous devez stocker sur la pile ce que le programme doit faire lorsque vous revenez d'un appel de fonction itéré (c'est ce qu'on appelle une continuation). Pour certaines fonctions (telles que les fonctions récursives de queue), la continuation est triviale. Pour d'autres, la suite peut être très complexe, surtout si vous devez l'encoder vous-même.
Dave Clarke
6

Q : "Existe-t-il vraiment une preuve plus formelle (convaincante?) Que la récursivité peut être convertie en itération?"

R : L'intégralité de Turing d'une machine de Turing :-)

À part les blagues, le modèle de machine à programme stocké à accès aléatoire (RASP) équivalent de Turing est proche du fonctionnement des microprocesseurs réels et son jeu d'instructions ne contient qu'un saut conditionnel (pas de récursivité). La possibilité d'auto-modification dinamique du code facilite la mise en œuvre des sous-programmes et des appels récursifs.

Je pense que vous pouvez trouver de nombreux articles / articles sur la " conversion récursive en itérative " (voir la réponse de Dave ou simplement Google les mots-clés), mais peut-être une approche moins connue (et pratique ) est la dernière recherche sur la mise en œuvre matérielle d'algorithmes récursifs ( utilisant le langage VHDL qui est "compilé" directement dans un morceau de matériel). Par exemple, voir l'article de V.Sklyarov "Implémentation basée sur FPGA d'algorithmes récursifs " ( L'article suggère une nouvelle méthode pour implémenter des algorithmes récursifs dans le matériel. .... Deux applications pratiques des algorithmes récursifs dans le domaine du tri et de la compression des données ont été étudiées en détail .... ).

Marzio De Biasi
la source
1

Si vous êtes familier avec les langages qui prennent en charge les lambdas, une solution consiste à étudier la transformation CPS. Supprimer l'utilisation de la pile d'appels (et la récursivité en particulier) est exactement ce que fait la transformation CPS. Il transforme un programme contenant des appels de procédure en un programme avec uniquement des appels de queue (vous pouvez les considérer comme des gotos, qui est une construction itérative).

La transformation CPS est étroitement liée à la conservation explicite d'une pile d'appels dans une pile traditionnelle basée sur un tableau, mais au lieu d'un tableau, la pile d'appels est représentée avec des fermetures liées.

Jules
la source
0

à mon avis, cette question remonte aux origines mêmes des définitions du calcul et a longtemps été rigoureusement prouvée à cette époque où le calcul lambda de l'église (qui capture très bien le concept de récursivité) s'est avéré être équivalent aux machines de Turing et est contenu dans la terminologie encore utilisée "langues / fonctions récursives". également apparemment une référence clé ultérieure le long de ces lignes est la suivante

Comme l'a souligné l'article de 1965 de Peter Landin, A Correspondance between ALGOL 60 and Church's Lambda-notation, les langages de programmation procédurale séquentielle peuvent être compris en termes de calcul lambda, qui fournit les mécanismes de base pour l'abstraction procédurale et l'application de la procédure (sous-programme).

une grande partie du bkd à ce sujet se trouve dans cette page wikipedia thèse église-turing . Je ne suis pas sûr des détails exacts, mais l'article de Wikipédia semble indiquer que c'est Rosser (1939) qui a le premier prouvé cette équivalence entre le calcul lambda et les machines de turing. peut-être / vraisemblablement que son papier a un mécanisme semblable à une pile pour convertir les appels lambda (éventuellement récursifs) en construction tm?

Rosser, JB (1939). "Une Exposition Informelle de Preuves du Théorème de Godel et du Théorème de l'Église". The Journal of Symbolic Logic (The Journal of Symbolic Logic, Vol. 4, No. 2) 4 (2): 53–60. doi: 10.2307 / 2269059. JSTOR 2269059.

notez bien sûr pour toute personne intéressée par les principes que le langage Lisp moderne et la variante Scheme ont intentionnellement une forte ressemblance avec le lambda calcul. l'étude du code d'interprète pour l'évaluation de l'expression mène à des idées qui étaient à l'origine contenues dans des articles pour l'intégralité du turing du calcul lambda.

vzn
la source
1
La preuve d'équivalence Turing / lambda est en annexe de cet article: www.cs.virginia.edu/~robins/Turing_Paper_1936.pdf
Radu GRIGore