• Aucun résultat trouvé

IFT2030 Langages de programmation TP2 par: Nicola Grenon GREN30077303 et Jean-François Delisle DELJ02057809 lundi vingt-sept mars deux mille six

N/A
N/A
Protected

Academic year: 2022

Partager "IFT2030 Langages de programmation TP2 par: Nicola Grenon GREN30077303 et Jean-François Delisle DELJ02057809 lundi vingt-sept mars deux mille six"

Copied!
1
0
0

Texte intégral

(1)

IFT2030

Langages de programmation

TP2

par:

Nicola Grenon GREN30077303 et

Jean-François Delisle DELJ02057809

lundi vingt-sept mars deux mille six

(2)

I – Le problème des reines sur un échiquier de taille variable.

1.1) L'approche

Pour solutionner le problème des ndames, il nous a d'abord fallu en simplifier la formulation. Une simple observation du fait qu'il ne peut jamais y avoir plus d'une reine sur une colonne permet de réduire le problème en terme de trouver la position d'une reine par rapport aux autres reines qui sont obligatoirement sur d'autres colonnes. Ceci nous a permis d'extraire une récurrence facilement applicable à notre situation. Ainsi, comme les mouvements d'une reine sont symétriques (une reine qui peut en manger une autre est elle-même forcément en danger) on peut se dire que si la reine placée sur la colonne i ne peut manger une de celles à sa droite, alors elle est elle-même hors d'atteinte.

On a donc utilisé un design de récurrence sur les colonnes qui cherche et trouve sur quelle(s) rangée(s) la reine de cette colonne peut être positionnée par rapport à toutes celles déjà positionnées dans les colonnes à sa droite. On n'a donc ensuite plus qu'à répéter l'opération, en corrigeant le tir au besoin en revenant sur nos pas quand il n'y a pas de solution, jusqu'à ce que qu'on ait déterminé (si elle existe) la rangée où l'on doit placer la reine pour chaque colonne de l'échiquier.

Ayant un échiquier de taille déterminée pour chaque occurrence du problème et comme il n'y a une et une seule position à mémoriser par colonne, nous avons utilisé une simple liste d'entier pour structure de donnée qui sera directement retournée en tant que solution du problème à l'utilisateur du programme.

1.2) Les problèmes et difficultés

Lorsque nous avons d'abord commencé l'élaboration de la solution, nous avions opté pour une approche où chaque ligne de danger générée par une reine était une entité individuelle (diagonale ers le haut, vers le bas et ligne horizontale), et si chaque ligne pouvait se rendre jusqu'après la colonne de droite sans rencontrer de reine, alors le test était réussi. Toutefois, pour synthétiser le problème, rendre le code plus lisible, facile à comprendre et facile à corriger, nous avons dû repenser notre solution pour synthétiser davantage les instructions de test. Ainsi, maintenant notre code ne contient que 4 lignes d'instruction principales dont deux qui englobent tous les tests en les unifiant. Pour ce faire il a toutefois fallu introduire plus de variables dans l'appel du foncteur de test.

Le principal obstacle que nous avons rencontré dans ce travail sur l'échiquier était en fait causé par un effet inattendu de bord sur la fonction d'addition que nous utilisions. En effet, pour «suivre» les lignes de danger des reines, nous utilisions l'addition pour la diagonale montante et à nouveau l'addition (inversée) pour la diagonale descendante. Or, comme notre condition d'arrêt était l'Atteinte de la dernière colonne, il advenait que la ligne descendante butait sur des valeurs qui auraient dues être négative, mais comme la fonction d'addition ne retourne pas de telles valeurs, cela faisait échouer un test qui aurait dû réussir. Notre solution fut de simplement créer une fonction de soustraction truquée pour nos besoins qui retourne toujours une valeur. En l'occurrence elle retourne 0 au lieu d'une valeur négative, mais cela n'a pas d'impact puisque notre grille est numérotée à partir de la rangée numéro 1.

(3)

1.3) Consignes

Notre solution au problème assure la correction des résultats puisqu'au niveau du déroulement des tests de validité de position, on a su représenter avec exactitude les contraintes du problème, sans compromis. Le foncteur de test peut se traduire directement comme une équivalence de l'énoncé du problème. Il serait ainsi aisé de prouver par induction que les résultats sont corrects.

La terminaison du programme est elle aussi assurée par le fait que nous utilisons clairement l'approche générer/tester. En l'occurrence, on génère seulement n possibilités par colonne, donc, une fois les nxn possibilités visitées, il n'y a plus d'autres possibilités à tester. On stop donc toujours le programme après ces nxn cas.

Évidemment ici on parle du cas où une valeur n est fournie en paramètre... si ce n'est pas le cas, toutes les solutions jusqu'à l'infini seront offertes, mais il est à noter que pour chacune il y aura un nombre fini de possibilités qui seront envisagées.

Le problème étant relativement restreint, nous avons pu conserver un style complètement pur de programmation logique. Le besoin d'utiliser des dérives impures qui était peut-être tentant au début n'est pas utile en fin de compte pour ce problème.

(Mais on ne peut pas dire que ce soit el cas pour le problème suivant!)

Pour nous assurer que nos réponses étaient uniques, nous avons employé deux méthodes. La première, naïve mais néanmoins efficace, fut de réduire le nombre de sous appels à des foncteurs de plus bas niveau après avoir remarqué combien il était facile de générer alors des boucles infinies. LA seconde, plus pragmatique, vient de l'observation qu'il est également malheureusement aisé, lorsqu'on multiplie les énoncés, de générer des solutions fantômes, en particulier lorsqu'un énoncé est dédoublé en un cas général et un cas de base. Il faut alors être très prudent et s'assurer que les énoncés sont mutuellement exclusifs. Notons ici qu'il a été pénible, dans un premier temps, de réaliser la provenance de ces effets. Prolog ne nous offre pas beaucoup de solutions pour tenter de trouver la source de ce genre de problème, c'est pourquoi nous avons dû implanter notre propre système de traçage dont nous parlerons plus en détail ci-bas.

La réversibilité, à savoir la possibilité d'employer le foncteur principal, avec l'un ou l'autre des arguments présent ou aucun des deux est assurée dans le programme par le fait que nous ne générons pas une infinité de possibilités ni dans un sens ni dans l'autre. En fait, la réversibilité dans le cas de ce problème est un élément plus facile à atteindre du fait que la structure de donnée sous-jacente est unidimensionnelle (une liste d'entiers).

L'exhaustivité est atteinte par le fait que toutes les possibilités sont examinées, tout simplement. Encore une fois le fait d'être sur un array à une dimension dont chaque valeur est clairement bornée par 1 et n simplifie grandement la tâche.

1.4) Prolog

Les avantages du Prolog dans l'élaboration de notre solution sont clairs juste au regard du code. Il a suffit de 4 lignes (principales) de code pour tout faire. On obtient donc une concision incroyable du code tout en conservant une grande lisibilité. La puissance de la réversibilité des appels est aussi indéniable. Ainsi, n'avoir qu'un appel à

(4)

faire pour générer toutes les solutions du domaine réalisable est vraiment «payant»

d'un point de vue retour sur investissement (d'efforts).

Ce qui rend toutefois l'utilisation de Prolog plus périlleuse est le grand effort d'abstraction qu'il nécessite du programmeur. Il nous semble que Prolog perd ici un des avantages obtenu avec les langages de programmation évolués, à savoir rendre le travail plus aisé pour le programmeur. Ici, le niveau de complexification est inouï.

Ajouter un petit détail entraîne une avalanche de conséquences. La modularisation du code est très difficile à obtenir à notre avis. Par exemple un simple changement d'ordre d'appel pour solutionner une question de génération de valeurs à tester peut entraîner un cataclysme au niveau des variables, car on vient souvent, en agissant de la sorte, de libérer une variable qui, devenant non liée, introduit des boucles sans fin.

Il est aussi très difficile de fonctionner (peut-être la force de l'habitude?) sans avoir de réel ordre des opérations déterminé. On ne peut pas facilement garantir un invariant ou du moins, il est excessivement facile de le détériorer sans s'en apercevoir.

Mais en ce qui nous concerne, le pire problème, dont nous avons fait mention un peu plus haut et auquel nous avons dû faire face est la quasi impossibilité de faire le traçage de l'exécution du programme. Le code étant synthétique et compacte, il est déjà dur de visu de voir d'où peut provenir une erreur, car on ne doit souvent pas chercher une ligne où se trouve une erreur, mais le morceau de ligne où elle se situe. Que dire donc de la fonction de traçage incluse dans Sicstus. Elle est de loin trop détaillée pour être utilisable de façon concrète. Nous avons donc dû en la matière nous faire notre propre système de traçage en employant des «write» que nous avons dû intercaler dans notre code en de multiples endroits. Ce n'est certes pas une façon très pratique de travailler, d'autant plus que le résultat était souvent ambigu à interpréter.

(5)

II – Le problème du labyrinthe.

2.1) L'approche

En ce qui a trait au problème du labyrinthe, nous avons, à force d'un dur labeur d'ingénierie, déterminé deux grandes d'en façon d'aborder le résolution.

La première, plus intuitive, consiste à parcourir un labyrinthe fourni comme le ferait une souris, en cherchant son chemin par essais et erreurs. On déterminerait alors un chemin à employer en évaluant d'une case à l'autre celles où on peut se rendre jusqu'à trouver finalement, après probablement plusieurs retours en arrière, un trajet nous menant jusqu'à la sortie à force de systématisme. À noter que comme notre labyrinthe a été défini comme un arbre, il nous suffit alors, pour éviter les boucles infinies, d'éviter que lors du choix d'une case on prenne celle d'où l'on vient.

L'autre solution serait d'aborder la question du point de vue d'un serpent gigotant dont la queue est attachée à l'entrée de notre rectangle de jeu. Notre algorithme générerait toutes les possibilités de trajet possible pour un espace donné de nxm. On pourrait ensuite comparer chacun de ceux-ci avec notre labyrinthe témoin pour vérifier sa validité. Bien sûr, cette manière de fonctionner est manifestement plus lente car un nombre impressionnant de mauvais choix sont faits puis rejetés. Mais l'avantage est que si on juxtapose ce générateur de chemin à un générateur de labyrinthe, on peut obtenir des solutions de façon systématique et symétrique en comparant les uns aux autres. Une astuce au niveau de la limite à respecter par ces deux générateurs nous permet même d'en assurer la compatibilité. Ainsi, si on génère en diagonal toutes les valeurs possibles de point de sortie d'un labyrinthe, on effectue un balayage exhaustif du domaine quel qu'il soit: [0|0],[1|0],[0|1],[2|0],[1|1],[0|2],[3|0],[2|1],... qui s'obtient en forçant la somme des coordonnées à croître d'au maximum une unité quand toutes les options ont été examinés. Alors si ce point de sortie du labyrinthe est le critère pour limiter le domaine des labyrinthes possibles et celui des chemins possibles, alors je peux énumérer exhaustivement les paires labyrinthe-chemin. Un avantage supplémentaire de cette approche c'est la généralisation du problème: on évite de limiter la question à des labyrinthe sans cycle. Il nous faudra par contre limiter la longueur du chemin au nombre total de case du labyrinthe.

2.2) Les problèmes et difficultés

La première difficulté à laquelle nous sommes confrontés est la complexité du problème. En apparence simple, il se subdivise en une multitude de contraintes se recoupant. Fonctionnellement parlant, il faut tout d'abord limiter les chemins valides à ceux qui ne passent pas par une case où ils sont déjà passé ni à côté d'une de celles- ci. Ensuite viennent les contraintes du style de programmation: étant maintenant dans un univers bidimensionnel, on doit travailler avec une liste de listes pour représenter les cases. Et on ne peut dynamiquement changer les valeurs de celles-ci. Tout doit donc être récursif. Ainsi, on ne peut pas imaginer un masque appliqué sur une copie du labyrinthe auquel on ajouterait des murs au besoin. Il faut donc toujours être à même de lancer le programme vers une vérification d tout ce qui a été fait avant plutôt que de comparer une valeur comme on le ferait avec un variable en Java par exemple.

(6)

Un autre blocage auquel on fait face quand on essaie de progresser ici est l'environnement à deux dimensions. Celui-ci nous force à générer des valeurs dans deux sens, ce qui, si on n'a pas de limite prédéterminée, crée des boucles infinies et/ou des problèmes au niveau de l'exhaustivité à cause de la multiplicité des variables non liées. Ici toutefois, l'idée d'un générateur en diagonal tel que décrit précédemment nous est d'une grande aide. Néanmoins, il reste un paradoxe fonctionnel à résoudre. Comme il n'y a pas vraiment un séquencement des opérations au sens de Java en Prolog, si on veut conserver la réversibilité, on a de la difficulté à obtenir l'information dont on a besoin pour commencer le calcul. C'est-à-dire que si je génère tous les trajets possibles en fonction d'un point de sortie, il m'a tout d'abord fallu savoir ce point de sortie. De même si je veux générer tous les labyrinthes possibles en fonction d'un point de sortie, il m'a aussi fallu connaître ce point de sortie à l'avance. Donc, si je ne connais ni le trajet ni le labyrinthe à priori, je ne peux pas avoir ms limites au calcul de l'un et l'Autre car je ne peux que les obtenir en les extrayant du point de sortie! Et pire encore, si je ne sais pas à l'avance si j'aurai en paramètre le labyrinthe ou le chemin, je ne peux pas ordonner correctement les opérations à l'intérieur du foncteur pour me servir du point de sortie extrait!! L'œuf ou la poule quoi.

2.3) Consignes

Ici encore, la correction des résultats est assurée par la précision de la reproduction des contraintes du problème. Prévoir tous les cas de figure en est la clé.

La terminaison du programme est ici bien plus ardue à assurer que dans el cas des reines. Comme nous l'avons mentionné déjà, le fait que nous devions généré des coordonnées au lieu d'une liste de valeurs rend le tout plus complexe. En utilisant une méthodologie de génération de possibilités en diagonal on diminue les risques, mais cela devient très difficile à gérer si on n'a pas de limites à donner à notre problème au départ.

La logique pure nous apparaît ici hors de portée. Il y a tellement de fonctions qui se croisent et qui n'ont pas obligatoirement de limites prescrites au départ, que pour forcer l'avancement du programme, tout en conservant un style lisible, nous devions utiliser l'outil de coupure.

L'idée de diminuer le nombre total de foncteurs afin de réduire les risques de dédoublement des réponses est plus difficilement applicable ici vu la complexité du problème et la nécessité de le subdiviser. Il nous faut donc porter une attention particulière aux cas de base des foncteurs et à l'ordre des opérations prescrites dans ceux-ci. On s'aperçoit ici qu'il devient très rapidement difficile d'éviter les erreurs e de passer un long temps à déterminer la source de celles-ci dès que le code a plus de quelques lignes. De plus, l'interdépendance marquée entre les diverses parties du problème a tendance à rendre encore plus fréquent la perte de vue de certaines variables dès qu'on modifie un tant soit peu le code et alors on se retrouve à nouveau avec plusieurs variables non liées.

L'idée de la réversibilité est ici le point le plus difficile à solutionner selon nous.

Comme nous l'avons déjà mentionné, le fait de ne pas savoir à l'avance par «quel bout»

le foncteur sera prit nous coince car il devient alors impossible de prédéterminer la valeur du point de sortie, ce qui est essentiel dans une optique de génération de toutes les solutions.

(7)

L'exhaustivité des solutions obtenues tient quant à elle en la capacité que nous avons eu à obtenir des solutions correctes et à les générer complètement. À partir du moment où l'on connaît les imites du domaine (par le point de sortie) alors on peut être sûr d'y arriver.

2.4) Prolog

Les avantages du Prolog pour la partir du labyrinthe de ce travail sont beaucoup plus difficiles à cerner. Oui on peut clairement découper le problème en ses parties qui se résolvent récursivement. Aussi on n'a pas à trop se préoccuper de la partie génération des possibilités dès qu'on en a établi les frontières du domaine. Le souci est toutefois justement d'arriver à l'établir. En programmation impérative, par exemple, nous pourrions aisément établir un scénario différent dépendamment des informations fournies. L'utilisateur m'indiquerait qu'il connaît déjà soit le labyrinthe, soit le chemin, ou aucun des deux. Alors on pourrait déterminer le point de sortie et générer 'autre branche ou au besoin générer tous les points de sortie possibles... ce qui semble mal aisé voire impossible en Prolog (à notre niveau en tout cas).

Notons que le Prolog nous permet toutefois d'aborder le problème de différentes manières, contrairement à ce qu'on pourrait croire à prime abord. Par exemple on peut choisir d'utiliser une méthode itérative ou par force brut et vérification.

Les désavantages dans ce cas-ci sont nombreux. La complexité liée au niveau d'abstraction nécessaire pour définir les opérations. La difficulté à garder le code fonctionnel dès qu'on veut le modifier un peu qui créé des explosions de solutions dupliquées ou répétitives à l'infini. La grande difficulté à visualiser l'avance des opérations en suivant le code manuellement ou même pas traçage.

Mais le pire problème reste la complexification: on ne peut terminer une parti du travail, la stocker et passer à une étape suivante. Le haut niveau d'abstraction nécessaire cache également un autre problème: on peut facilement se croire au bord d'une solution avant de se retrouver dans un cul-de-sac logique.

III – Conclusion

Pour conclure nous nous prononcerions sur le fait que le Prolog est incontestablement très puissant et permet de modéliser des idées complexes aisément dans certains cas, mais qu'il peut s'avérer complexe d'essayer de modéliser des idées pourtant simples. Donc il serait judicieux de garder l'esprit ouvert quant au langage à utiliser pour résoudre un problème. Prolog est manifestement performant pour des problèmes ayant une similarité avec le concept mathématique d'induction, mais plus on s'en éloigne plus il semble avantageux d'y préférer un langage d'un autre type, à tout le moins pour ce qui est de la facilité de conception.

Références

Documents relatifs

La seconde partie effectue une fouille en profondeur en marquant chaque nouveau noeud rencontré d'une valeur alternant entre gauche et droite. Si en

Comme on applique au passage à chaque dessinateur les transformations que devra subir le dessinateur final, on obtient deux listes de

Chaque fois qu'on voudra ajouter une chaîne de caractère à notre base de données, on vérifiera donc si elle est déjà présente dans la liste de toutes celles déjà employées et

Donc: vote, électeur, carte d'identité, liste électorale, élection, liste des élections, liste d'émargement, option de vote....

Le logiciel affiche un message d'information, ferme la session et après quelques secondes réinitialise l'écran.. 

Le logiciel affiche un message d'information, ferme la session et après quelques secondes réinitialise l'écran.

Les paires de parenthèses n'ont pas besoin d'être une classe, mais un simple attribut de l'expression. On pourrait utiliser un booléen pour indiquer lorsqu'on veut

Il n'est pas clair ici si en allant dans cette section si on quittera le site vers une page publicisée externe ou si on restera dans une sous-section du site