Le Sat, 24 Jul 2021 19:39:29 +0000, Stephane Tougard a écrit :
> On 2021-07-24, Stéphane CARPENTIER <
s...@fiat-linux.fr> wrote:
>> Normalement, je dirais que tu peux toujours éviter d'utiliser une
>> boucle en utilisant la récursivité. Le problème de la récursivité,
>> c'est que ça peut être très gourmand en mémoire.
Ça entasse surtout des données dans la pile, là où on stocke les
variables locales et les adresses de retour des fonctions (pour mémoriser
l'endroit auquel il faut reprendre une fois qu'on a terminé d'exécuter
une fonction appelée). L'espace alloué à la pile étant limité, un trop
grand nombre d'appels de fonctions imbriqués peut la faire déborder. Il
reste plein de mémoire disponible dans la machine, ce n'est pas le
problème (en tout cas sur des ordinateurs modernes, sur des machines
anciennes je ne sais pas).
On peut rappeler que le tas, par opposition à la pile, est une zone dans
laquelle on alloue des données n'importe où et sans limite de durée
(alors que les données placées sur la pile par une fonction disparaissent
quand la fonction se termine).
Quelques articles trouvés rapidement, si ça vous éclaire :
https://www.journaldunet.fr/web-tech/developpement/1202909-que-sont-
la-pile-stack-et-le-tas-heap/
https://n-pn.fr/t/2431-la-pile-en-assembleur-x86
https://www.fil.univ-lille1.fr/~sedoglav/C/Cours08-2x3.pdf
> D'après ce que j'ai compris, au niveau de la machine, il n'y a pas de
> différence fondamentale entre une fonction récursive et une boucle, les
> deux utilisent les mêmes code en language machine.
Normalement si :
- Une boucle est traduite de manière directe, avec des branchements
("goto").
- Dans le cas d'un appel récursif, le code exécuté entasse bien sur la
pile les variables locales et les adresses de retour (il y a bien
un mécanisme d'appels de fonction dans le code machine, on peut voir
ça dans les articles ci-dessus).
Par contre dans certains cas le compilateur peut traduire un appel
récursif en boucle : si la dernière chose qu'une fonction doit exécuter
est un appel récursif, il n'y a pas besoin de conserver les variables
locales et l'endroit où on se trouvait dans la fonction (puisqu'on n'a
plus rien à y exécuter). Dans ce cas, le compilateur peut générer du code
exécutable sans appel de fonction.
Petit exemple avec la factorielle...
(je vire les cas d'erreur pour simplifier)
Version classique :
(defun factorielle (n)
(if (= n 0)
1
(* n (factorielle (1- n)))))
Version avec accumulateur :
(defun factorielle (n &optional (accumulateur 1))
(if (= n 0)
accumulateur
(factorielle (1- n) (* n accumulateur))))
Dans la deuxième version on voir qu'une fois l'appel récursif terminé il
n'y aura plus rien à faire en revenant à la fonction appelante (rien
d'autre que tout dépiler pour renvoyer le résultat), donc le compilateur
peut éviter d'entasser sur la pile de nouvelles variables locales et
l'adresse de retour et se contenter d'un code similaire à celui d'une
boucle.
En Common Lisp ce n'est pas pas garanti (contrairement à Scheme je
pense), mais en pratique les compilateurs le font en général. Sauf peut-
être dans le cas où on les configure pour le débogage plutôt que pour les
performances, parce qu'on peut préférer garder les appels récursifs dans
la pile pour les traces en cas d'erreur.
> Peut-être ai-je tout faux sur ça.
>
> En attendant, essaye de faire ça sans la récursivité :
>
> (defun parse-j(j s)
> (if (or (not (listp j)) (null j)) ()
> (progn
> (if (and (listp (car j)) (equalp (caar j) (car s)))
> (if (= (length s) 1)
> (car j)
> (parse-j (car j) (cdr s)))
> (parse-j (cdr j) s)))))
>
>
> Ça sert à parser une liste retourné par la fonction decode-json du
> package cl-json.
>
> (parse-j a '(:DATA :XPUB :XPUB :BALANCE)
>
> Ou a est la liste en question et la liste correspond à l'entrée
> recherchée dans cette liste.
En théorie il me semble qu'il est toujours possible de transformer une
boucle en appel récursif et inversement. Pour ce deuxième cas, en général
il faut gérer une pile comme structure de données dans le code et ça peut
être plus ou moins compliqué selon les situations.
On remarque que dans le cas de ton code les appels récursifs sont des
appels terminaux, donc la traduction en boucle ne doit pas être
difficile. Exemple en remplaçant simplement l'appel récursif par une mise
à jour des valeurs locales de j et s (et en passant implicitement à
l'itération suivante). Note : (return ...) sort de la boucle, pas de la
fonction ; ici c'est pareil mais sinon il faudrait utiliser (return-from
parse-j ...)).
(defun parse-j (param-j param-s)
(do ((j param-j)
(s param-s))
(nil)
(if (or (not (listp j))
(null j))
(return ())
(if (and (listp (car j))
(equalp (caar j) (car s)))
(if (= (length s) 1)
(return (car j))
(progn
(setf j (car j))
(setf s (cdr s))))
(progn
(setf j (cdr j))
(setf s s))))))
Il se peut que je n'aie pas compris à quoi sert la fonction, mais j'ai
ces résultats :
- Ce cas me renvoie un résultat :
(parse-j '((:data) :truc) '(:DATA))
- Ces cas ne me renvoient rien :
(parse-j '(:bidule (:machin (:data)) :truc) '(:DATA))
(parse-j '((:d1 :d2) :truc) '(:d1 :d2))
(parse-j '((:d1 :d00) (:d1 :d2) :truc) '(:d1 :d2))
Et dans le code initial, le progn me semble inutile (il ne fait
qu'enfermer un unique if) et le code est mal indenté. Il se peut que ce
ne soit qu'un détail et que la fonction effectue correctement son
traitement, ou bien qu'il s'agisse d'une erreur et que le code ne fasse
pas ce qui est prévu, je ne sais pas.