Programmation Avancée
TP4: effets et contrôle
Dans ce TP nous allons d'abord dérouter le flot d'exécution de prédicats afin d'implémenter un test d'égalité apparemment impossible, puis mettre en place un flot d'exécution complexe dans un algorithme avec backtracking.
Exercice 1: effets espions
Une fonction est totale si elle termine normalement (sans lever une exception) sur toute entrée. Elle est pure si elle ne manipule pas d'état, d'exceptions, etc. Dans ce cas son résultat dépend uniquement de la valeur passée en entrée.
On définit un type pour représenter des prédicats: type 'a pred = 'a -> bool
.
Question 1
Ecrire une fonction qui calcule l'égalité extensionnelle entre deux
prédicats purs et totaux sur les booléens:
f (p : bool pred) (q : bool pred)
vaut true
si et seulement si p x = q x
pour tout (x : bool)
.
Question 2
On représente un flux de booléens par une fonction de type
stream = int -> bool
, et on veut maintenant calculer l'égalité entre
prédicats purs et totaux sur stream
. Le problème est qu'il y a une infinité
de stream
possibles, on ne peut donc comparer comme avant les deux prédicats
sur toutes les entrées possibles.
Sur une entrée donnée, un tel prédicat va évidemment accéder seulement à un nombre fini de valeurs du stream, dépendant potentiellement du stream. Montrez que ce nombre est en fait borné indépendamment du contenu du flux.
Pour s'échauffer, implémentez à l'aide du mécanisme d'exceptions un objet de
type stream
qui permette de détecter si un prédicat sur stream
accède au
contenu du stream
qui lui est passé en argument.
En généralisant, écrivez une fonction permettant de déterminer si un prédicat
(pur et total) sur stream
renvoie true
sur toute entrée.
Il est ensuite immédiat d'obtenir une implémentation de l'égalité
extensionnelle sur stream pred
.
Question 3
Peut-on faire la même chose sans utiliser les exceptions?
Exercice 2: le problème des reines
Ce problème classique consiste à poser n
reines sur un échiquier de taille n
de façon à ce qu'aucune reine ne soit menacée par une autre: on doit donc
avoir exactement une reine par ligne, une reine par colonne et une reine par
diagonale montante ou descendante.
Il n'existe pas de formule magique pour énumérer, ou même compter ou décider l'existence d'une solution. Il n'y a pas d'autre choix que tester toutes les tentatives de construction de solutions possibles. On fera ceci incrémentalement, ligne par ligne, en posant à chaque étape une reine sur la première ligne vide, et en éliminant un échiquier dès lors que deux reines posées se menacent (on coupe ainsi au plus tôt une branche de l'exploration, qui correspond potentiellement à de multiples échiquiers comportant une reine par ligne).
Quand on programme de tels algorithmes, il est très inefficace de représenter explicitement les collections d'objets que l'on énumère: cela prendrait trop de place en mémoire, et c'est inutile car on ne traite jamais qu'un objet à la fois. On va donc générer les objets les uns après les autres, en "backtrackant" sur les choix faits lors la génération. D'une certaine façon, on remplace le calcul d'une liste de possibilités par la définition d'un itérateur sur cette liste.
Question 1
Implémenter une solution sous la forme d'une fonction qui prend en
argument la taille du problème et une fonction à appeler sur chaque solution
trouvée: reines : int -> (board -> unit) -> unit
.
La fonction procèdera par construction progressive d'une solution, en posant des reines ligne par ligne sans avoir de conflit, et en "backtrackant" quand on se retrouve avec une ligne sans reine possible. Pour être efficace, on maintiendra en même temps que la solution en cours de construction l'ensemble des alignements déja pris, les alignements étant ici les lignes, colonnes, et diagonales.
Question 2
Utiliser votre algorithme pour afficher la première solution et arrêter le calcul ensuite. Séparemment, l'utiliser pour compter le nombre de solutions. Vous pourrez comparer vos valeurs avec les résultats officiels.