Haskell | Mise à jour 06-2010 |
|
Il existe plusieurs paradigmes de programmation. Les plus connus sont :
Basé sur des concepts mathématiques, il présente un aspect plus déclaratif, c'est-à-dire que l'ordre des instructions est indifférent. Par exemple, quand on écrit classiquement : x=1; y=2*x, en haskell on peut l'écrire ainsi ou à l'envers : y=2*x; x=1. Fonctionnel signifie que les traitements sont réalisés par l'intermédiaire de fonctions, qui ne dépendent que des paramètres qu'elles reçoivent, et non de variables extérieures. Un programme est donc un enchainement de fonctions. La transparence référentielle stipule qu'une fonction, pour des paramètres donnés, doit toujours donner le même résultat. Une conséquence surprenante : les variables ne sont pas modifiables, donc pas d'effet de bord. Alors, comment incrémente-t-on une variable ? On écrit f x = x + 1 et on passe cette fonction à la fonction désirée. Et une boucle ? On écrit une fonction récursive, c'est-à-dire qui s'appelle elle-même. L'exemple classique est la factorielle n! = n * (n-1) * (n-2) ... * 1 que l'on peut exprimer aussi n! = n * (n-1)! Exception notable à la transparence référentielle : les entrées-sorties. En effet, une lecture-clavier , sans paramètre d'entrée, donne un résultat toujours différent (la saisie effectuée). Il existe des mécanismes pour tenir compte de cette particularité et ne pas "casser" l'aspect fonctionnel du programme. Les listes constituent une structure de données essentielle en Haskell. Ici encore, les fonctions récursives permettent de balayer les listes et de réaliser des traitements sur celles-ci. Enfin, Haskell est un langage à évaluation "paresseuse" : les fonctions sont évaluées lorsque nécessaire. Par exemple si on a : a=1; b=2; c=b+1, l'évaluation de a n'est pas nécessaire dans le calcul de c, donc ne sera pas exécutée. Ce mécanisme autorise Haskell à définir des listes infinies, qui devront être limitées au moment de leur évaluation. Ainsi, la liste des entiers impairs s'écrit [1, 3, ..], le .. signifiant à Haskell de "continuer la liste".
Les données et fonctions sont typées; soit des types prédéfinis (entiers, flottants, booléens, caractères, ...), soit des types créés par le programme. Il existe des types récursifs, par exemple un arbre est constitué de feuilles et de sous-arbres, eux-mêmes etc... La compilation du source contrôle que les appels de fonctions respectent les types définis. Haskell facilite cependant la tâche en pratiquant l'inférence de type, c'est à dire en "devinant" le type selon le contexte. Quoiqu'il en soit, préciser le type permet de clarifier le source. Au niveau des structures de données, on trouve :
Haskell est implémenté de manière modulaire, avec un Prélude, que l'on peut compléter (import) de multiples modules et packages, qui apportent de nouveaux types et fonctions. Voir la documentation en ligne via hoogle (référence en fin de page) pour des détails sur ceux-ci. A voir également deux points importants : les monades et les arrows. Enfin, une petite partie est consacrée à un environnement graphique, wxHaskell. |
Les quelques exemples ci-dessous sont l'occasion de tester certaines fonctionnalités du langage. Ils ont suivi des premiers pas hésitants, qui ont servi à découvrir les fondements du langage, et sont à ce titre révélateurs des questions qui viennent à tout débutant, lorsqu'il commence à créer de "vrais" programmes.
Le programme réalise un jeu de pendu. On y trouve la logique classique d'un programme :
Et aussi des manipulations de listes multiples : les lettres restantes, le mot à trouver, la solution en cours de constitution. On peut appeler le programme en lui passant le mot à trouver (secret xxxxxxxxxx) ou en lui laissant réaliser cette fonction de saisie (lancer par main). Ce programme est une de mes premières réalisations et les commentaires apportés par des participants de la mailing list ne sont pas la partie la moins intéressante. Certaines fonctions conçues de façon naïve peuvent ainsi être écrites plus intelligemment.
Le programme a pour objectif de transformer sous forme littérale un nombre donné. Il gère plusieurs langues. Les différentes parties du source utilisent diverses ressources courantes, listées ci-dessous. Chargement des paramètres
L'intérêt du programme réside dans l'implémentation d'une fonction de trace qui s'insère et s'enlève très facilement et est plus simple que l'emploi du debug interactif. En effet, l'ajout d'une trace par un simple putStrLn change le type de la fonction testée, qui devient IO (xxx) et doit être réécrite sous forme de do. Cette modification impacte le reste du programme, ce qui rend complexe le débogage du programme. Pour illustrer ceci, un exemple simple de traitement itératif permet de mettre en oeuvre cette trace. Le programme doit compléter une liste de valeurs, dont certaines sont à zéro. Pour ceci, il doit essayer successivement les valeurs issues d'une liste des disponibles, en respectant l'unicité des valeurs dans la liste finale. Utilisation de debug L'instruction debug est utilisée dans sa forme infixe caractérisée par
Le programme teste les fonctions d'accès aux fichiers :
Le programme résoud les puzzles numériques proposés par le jeu TV "Des chiffres et des lettres". A partir de 6 nombres, dont certains peuvent être égaux, il s'agit de reconstituer une valeur donnée, en utilisant au plus une fois chaque nombre, et en les combinant avec les quatre opérations de base. Chaque combinaison correcte est enregistrée dans un fichier texte. Le principe de calcul est celui-ci :
Ce programme met en jeu :
Le jeu consiste, sur un tableau carré de 9 cases, à reconstituer une configuration de couleurs déterminée, les cases présentant au départ deux états, allumé ou éteint. En choisssant une case, sa couleur et celle des cases voisine change; mais toutes les cases n'ont pas le même comportement vis à vis de leurs voisines. Ce programme met en jeu :
Il s'agit d'un solveur de Sudoku; il applique une méthode de recherche systématique des solutions en essayant pour chaque case les valeurs de 1 à 9. Lorsqu'une valeur est acceptable, les cases suivantes sont examinées de la même manière. Lors d'un blocage, le programme retourne à la case précédente pour essayer une autre valeur. La fonction de calcul est récursive, c'est ce qui permet la navigation d'avant en arrière dans la recherche des solutions. La difficulté dans ce genre d'algorithme est de trouver la fonction qui constitue le coeur du problème. Un autre point intéressant est l'utilisation - sacrilège dans Haskell - d'un tableau modifiable qui simplifie considérablement le traitement en évitant des calculs fastidieux de nouveaux tableaux et leur passage comme paramètres. Ce programme n'est pas de ma conception, mais a été publié sur le site haskelwiki dont les références sont plus loin. Sa concision est assez étonnante et sa vitesse d'exécution satisfaisante, même sur une machine ancienne.
Il s'agit du problème n° 99 des Ninety-Nine_Haskell_Problems. La solution est ici élaborée en deux phases :
Une solution plus efficace consisterait à ne générer directement que les 96 cas. Les opérations de génération des permutations, de filtrage sur longueur et sur lettres communes sont réalisées par un code très simple. La plus grande partie du code est dédiée à la transformation du tableau des cases en blocs (ligne, colonne, longueur et orientation) et en cases communes aux blocs 2 à 2, ainsi qu'à l'affichage de la solution sous forme de grille. |
Les exemples qui suivent mettent en oeuvre l'environnement graphique wxHaskell, basé sur la bibliothèque wxWidgets. Il n'obéit pas au paradigme fonctionnel (les valeurs et attributs des widgets sont modifiables) mais il permet cependant la création de formulaires avec les contrôles habituels (boutons, listes déroulantes, etc ...) Les fonctionnalités de ce GUI font l'objet de deux modules à incorporer dans les programmes, dont le deuxième s'appuie sur le premier pour donner des fonctions un peu plus "sympathiques" :
Ce programme montre, avec une application simple, l'utilisation de différents types de widgets :
Ce programme est une reprise du programme cases.hs qui est, lui, en mode console. La principale particularité est qu'il gère les boutons représentant les cases dynamiquement, c'est à dire qu'ils ne sont pas définis un à un par leur nom, mais dans une boucle qui retourne un tableau de références sur ces boutons. Les boutons sont anonymes. On peut ainsi imaginer des applications où le nombre et la disposition des widgets ne sont pas pré-déterminés mais dépendent du contexte.
Cette application permet de gérer une bibliothèque avec une interface graphique, en s'appuyant sur une base de données. Elle nécessite pour ceci d'utiliser un menu, plusieurs frames, une base de données, des listes déroulantes à partir de la bd, ceci à travers plusieurs fonctions :
Un écran principal présente la liste des livres. Des boutons et des menus permettent d'accéder à des fonctions supplémentaires (création, recherche, tri, exportation). D'un point de vue technique, plusieurs choix ont été effectués :
Cet écran possède deux fonctions principales :
La requête sql est modifiée par la fonction de recherche, qui choisit les critères de sélection (optionnels), et par la fonction de tri, qui choisit l'ordre de classement des livres sélectionnés. Elle est stockée sous forme de variable mutable. Dès qu'une modification des critères est validée, la liste des livres est actualisée, ainsi qu'après une création ou modification de livre. La requête est exécutée par la fonction dbQuery qui, pour chaque ligne lue, exécute une fonction chargée d'extraire les données par des instructions dbRowGetxxxxx, données retournées sous forme d'un tuple, et accumulées dans une liste. L'écran est complété de :
L'écran est un frame affiché sous forme de boîte de dialogue modale. Ceci nécessite de référencer l'écran principal et de créer un frame fictif pour éviter des problèmes d'écrasement. Les critères possibles, dont les valeurs sont optionnelles, sont :
La valeur de sélection est un n° de rang dans la liste. Il faut donc gérer parallèlement à la liste une liste des clés des arguments de la liste, de manière à trouver à partir d'une clé son n° d'ordre, et à partir d'un rang, la clé correspondante. Ces fonctions sont inscrites dans le module BiblioGeneral.hs. L'affichage modal s'effectue par la méthode showModal, qui a pour parcularité de passer en paramètre (souvent appelé stop) une fonction qui sera utilisée pour retourner la valeur finale issue du traitement de la boîte de dialogue. Cette valeur est de type Maybe; en cas d'abandon du dialogue, Nothing est retourné. En conséquence, le bouton OK constitue une valeur Just contenant les critères de recherche, tandis que le bouton Annuler retourne Nothing. Fonction de tri (module BiblioTri.hs) Le fonctionnement est analogue à celui de l'option de recherche. Le choix du type de tri s'effectue par des boutons radio groupés par un radioBox. Fonction d'export (module BiblioExport.hs) La requête actuelle extrait les livres et les copie dans un fichier texte; les champs sont séparés par des tabulations et l'extension est xls, ce qui permet une ouverture directe par excel. Une boite de dialogue fileSaveDialog permet de choisir le fichier de destination. Fonction de création/modif de livre (module BiblioCreation.hs) La création est accessible par une option de menu. La modification l'est par un double-clic sur une ligne de la liste des livres. La différence réside dans le paramètre n° de livre reçu: 0 en création, n° du livre choisi sinon. La présentation du frame est très semblable à l'écran de recherche, à la différence près que les listes déroulantes pour les auteurs et les types ne proposent pas de valeur vide (pas de critère optionnel). Là encore, les fonctions générales sur les listes déroulantes sont mises en application. Les données sont vérifiées avant d'être inscrites dans la base; un message infoDialog est affiché en cas d'anomalie et le curseur revient dans le champ erroné. Fonctions d'usage général (module BiblioGeneral.hs) Ce module contient des procédures utilisées dans plusieurs programmes ou modules. On y trouve :
Le programme a été réalisé avec une base access 97. Cependant, des tests ont été réalisés avec sqlite. Pour ceci, après avoir créé une base dans sqlite et le lien odbc correspondant :
|
GHCi 6.10.4, utilisé ici sous windows xp. HUGS L'interpréteur GHCi comporte le compilateur ghc, qui permet de créer des .exe Disponible gratuitement en téléchargement.
Ces sites diffusent des cours de différents niveaux sur le langage. developpez.com wikibooks site du zéro, initiation Haskellwiki Hoogle référence des modules et types de Haskell Exercices Sudokus en haskell 99 problèmes Mailing list, internationale et active.
Les programmes peuvent être compilés sous forme de fichier exe. Le source doit pour ceci comporter une fonction main. Un fichier compil.bat lance la création de l'exécutable. Celle-ci est déclenchée en tirant le module principal sur le raccourci du bat. set chem=%cd% set shs=%1 ghc --make %shs% -i%chem% -o%shs:hs=exe% del %chem%\*.hi del %chem%\*.o pauseL'astuce du raccourci est nécessaire pour que %cd% récupère le chemin du module principal, où sont également stockés les modules annexes. Ce chemin figure dans le répertoire de travail du raccourci. Dans le path, il faut avoir le répertoire haskell\bin. |