Voilà quelques jours, sous le coup de l'ennui couplé à la fatigue des partiels, je me suis dit « tiens, pourquoi ne pas coder un petit jeu de morpion, là comme ça, vite fait ». Direct j'vais voir Emacs (Super-g emacsclient -c dans mon Dwm favori), je lance SLIME, et je commence à taper sans réfléchir.

Tout se passe bien, tout est facile et rapide à écrire, jusqu'à… la boucle principale. Et voilà le problème : cette boucle doit appliquer une fonction (play <state> <joueur>), qui demande au joueur son coup, et en prend note, aux deux joueurs du morpion jusqu'à ce que la partie soit terminée. C'est tout con me direz vous, un petit coup de

(loop with joueur = 'X
   and state = '()
   while (game-not-finished state) do
     (setf state (play joueur))
     (setf joueur (if (eq joueur 'X) 'O 'X)))

et le tour est joué.

Oui, sauf que… c'est… comment dire… ben c'est moche, quoi. Et à quoi ça sert de coder un morpion lorsqu'on sait déjà le faire, si c'est pas pour arriver à quelque chose de joli ? Y m'faut une solution… fonctionnelle.

Alors là, je demande leur avis aux copains de #sdz-unix@irc.epiknet.org, et on me répond trois choses :

  • qu'est-ce que tu te fais chier, la boucle impérative ça marche très bien

(lastsounet)

  • utilises les continuations (lasts aussi)
  • t'as qu'à foutre ça dans une Monade et rulz (ah, gnomnain)

S'en suivent deux jours d'intense activité neuronale, durant lesquels je tente inutilement de comprendre l'intérêt des monades (j'avais déjà tenté le coup pour les continuations, sans succès). Découragé, je reviens à mes considérations lispeuses : rien ne vaut une bonne petite récursion.

Alors alors, une boucle normalement ça se traduit comme l'application d'une fonction à chaque élément d'une liste. Ici notre fonction, c'est play, et notre liste, c'est… (X O X O X O X O …) jusqu'à ce que le jeu soit terminé. Alors soit on construit notre liste avant, ce qui n'est ici pas possible, soit on décide en « temps réel » du moment où l'on doit s'arrêter. En fait, ce dont on a besoin, c'est donc d'appliquer notre fonction play à chaque élément d'une liste cyclique (X O), en vérifiant à chaque fois que le jeu n'est pas terminé. On a donc besoin de connaître à chaque instant l'état de la partie. Hum…

En fait, ce dont on a besoin, c'est d'une espèce de mélange entre reduce — pour parcourir une liste, appliquer une fonction à chaque élément de cette liste, tout en gardant le résultat cumulé de tous les appels — et some — pour parcourir une liste jusqu'à ce qu'un prédicat devienne vrai.

Déjà pour commencer, prévenons SBCL que nous allons utiliser des listes cycliques (sinon, il n'est pas content et boucle comme un neuneu) :

(setf *print-circle* t)

Ensuite, une petite fonction bien utile :

(defun cyclic (lst)
  (let ((cyc (copy-list lst)))
    (setf (rest (last cyc)) cyc)))

qui visiblement, fonctionne comme il faut :

CL-USER> (cyclic '(a b))
#1=(A B . #1#)

et puis, notre mélange entre reduce et some :

(defun cyclic-reduce (function sequence pred &key (initial-value
                                                   nil initial-value-supplied))
  "Like reduce, but stops its computing when the predicate applied to the
   result of the computing becomes true."
  (labels ((g (seq result)
             (if (or (null seq) (funcall pred result))
                 result
                 (g (rest seq) (funcall function result (first seq))))))
    (if initial-value-supplied
        (g sequence initial-value)
        (g (rest sequence) (first sequence)))))

Et voilà ! Maintenant, pour notre boucle principale, il suffit de construire une liste cyclique à partir de la liste des joueurs/euses, et de la donner à manger avec notre fonction play à notre cyclic-reduce. Mattez-voir comme c'est mignon tout plein :

(reduce-with-end #'play (cyclic players) #'game-over :initial-value nil)

Je kiffe.