Une Introduction à Haskell |
Partie 1: Pourquoi Haskell
Permettez-moi de commencer par être parfaitement clair : si vous êtes programmeur professionnel, alors Haskell fait partie de votre avenir.
En 1987, cette déclaration aurait été tout aussi vrai à propos de Smalltalk. Aujourd'hui, 20 ans plus tard, la programmation orientée objet, les hiérarchies de classe, l'architecture modèle-vue-contrôleur, et d'autres idées de Smalltalk sont devenus monnaie courante, même si Smalltalk n'a jamais vu son adoption généralisée.
Haskell est dans une situation similaire aujourd'hui. Il s'agit d'un langage ésotérique, mais stable et suffisamment fiables pour un travail sérieux. Peut-être que vous allez apprendre Haskell, ou même utiliser Haskell sur un projet dans le monde réel. Mais même si vous ne connaissez-pas les idées de Haskell, celles-ci sont lentement entrain de faire leur chemin dans les principaux languages, comme Java, C#, C++, Perl, Python et Ruby. Haskell est prêt à avoir une forte influence au cours des 20 prochaines années sur la programmation et dans la conception des langages, comme Smalltalk a eu une influence au cours des 20 dernières années.
Qu'est-ce que Haskell ?
Haskell est renommé, mais qu’est-ce qui le rend si spécial ? : La brève définition de Haskell par haskell.org dit :
Haskell est un langage de programmation purement fonctionnel mettant en vedette le typage statique, les fonctions d'ordre supérieur, le polymorphisme, les classes de type et les effets monadiques.
Si vous êtes universitaire ou concepteur de language, ces termes peuvent vous parler. Si vous êtes programmeur professionnel, des termes comme typage statique et polymorphisme peuvent vous sembler familiés, même si cette définition n'est pas particulièrement éclairante.
Fondamentalement, Haskell est un langage de programmation fonctionnelle, ce qui signifie qu'il est fondé sur une forme de lambda-calcul. Si cela vous semble intimidant, alors n'ayez pas peur, parce que le lambda-calcul n'est pas difficile du tout. Le lambda-calcul n'est rien de plus qu'un ensemble de règles précises qui décrivent la façon d'évaluer les fonctions. Il y a deux procédés en action (la définition de fonction et l'évaluation de fonction) et deux expressions en constructions (fonctions et valeurs). Si vous êtes un programmeur professionnel, vous avez déjà toutes les compétences dont vous avez besoin pour comprendre le lambda-calcul. Par exemple, dans n'importe quelle language de type C, la règle d'évaluation de cette déclaration est déjà présente profondément dans votre conscience :
printf("%.03f\n", sqrt(incr(1)));
La règle est d'évaluer l'ensemble des arguments d’une fonction d'abord, puis d'évaluer la fonction avec les valeurs résultantes. Ce processus est répété de façon récursive, jusqu'à ce que l'expression donne une valeur unique. Dans l'exemple ci-dessus, la fonction la plus intérieure,
incr(1)
est évaluée et produit la valeur2
. Ensuite,sqrt(2)
est évalué et produit1.414...
. Enfin,printf
est évaluée avec les deux arguments"%.03f\n"
et1.414...
pour produire une sortie formatée.Il y a un peu plus de fonctionnalités clé dans le lambda-calcul, comme la capacité à créer des fonctions, d’avoir des fonctions en résultat, et la capacité de définir des fonctions qui prennent des fonctions comme arguments. Ces caractéristiques permettent de définir les fonctions modulaires petites et de les coller dans des fonctions hautement spécialisées avec un minimum d'effort.
Par exemple,
filter
est une fonction d'usage général qui accepte comme arguments, une fonction de test (un prédicat), une liste de valeurs, et qui retourne une nouvelle liste contenant toutes les valeurs où le test est réussi.. La fonctioneven
est un de ces essais, qui détermine si un nombre est divisible par 2. Voici comment ces deux fonctions sont utilisées ensemble dans Haskell, pour extraire les numéros paire d'une liste de valeurs. Cet exemple utilise ghci, l'interprète interactifs fournis avec le Glorieux Glasgow Haskell Compiler (ghc) :
Prelude> filtre even [1..10] -- filtre les nombres pairs sur une liste
[2,4,6,8,10]Si je veux définir une fonction qui prend une liste de valeurs, et ne renvoie que les valeurs pairs de cette liste, Elle ressemblera à ceci dans l'interpréteur :
Prelude> let evens = filter even -- definie la fonction
Prelude> evens [1..10] -- filtre les nombres de la liste
[2,4,6,8,10]Cet exemple peut paraître trivial, mais il montre un des avantages du lambda-calcul dans un langage de programmation fonctionnel - la possibilité de créer des fonctions spécialisées simplement en assemblant un petit nombre de fonctions pré-existantes généralistes.
Cela est intéressant, c'est exactement comme un travail de pipeline opéré par un shell.
Écrire la fonction
evens
en C est beaucoup plus complexe. Tout d'abord, la liste de sortie doit être allouée, et pour ce faire, le nombre de valeurs de sortie doit être comptées. Ensuite, les valeurs doivent être copiés sur la nouvelle liste, et une à la fois. Cette simple expression Haskell devient la fonction C suivante :
int *evens(int *in, int count) {
int matches = 0;
int i, *out, *ptr;
for (i = 0; i < count; i++) {
if ((in[i] % 2) == 0) {
matches++;
}
}
out = malloc(sizeof(int) * matches);
ptr = out;
for (i = 0; i < count; i++) {
if ((in[i] % 2) == 0) {
*ptr = in[i];
ptr++;
}
}
return out;
}Cette fonction définit le comportement combiné de la définition Haskell de
evens
. Ce bloc de code est équivalent à l'expressionfilter even
en Haskell. S'il y avait une fonction dans la bibliothèque C qui traitait le comportement dufilter
, ce code serait plus simple. Toutefois, ce type de solution est malheureusement idiomatiques dans les langues de type C.Écrire cette fonction dans un langage plus moderne comme Java, C++ ou C# n'est pas aussi odieux, parce que la gestion automatique de la mémoire se charge de la première moitié de cette fonction. Écrire l'expression « filter even [1..10] » dans des langages dynamiques tels que Perl, Python et Ruby est plus proches de la définition Haskell, parce qu'ils définissent des fonctions primitives qui agissent comme des filtres. Voici à quoi ressemble cette expression dans les langues suivantes :
## Perl
grep {$_ % 2 == 0} (1..10)
## Python
filter(lambda n: n % 2 == 0, [1,2,3,4,5,6,7,8,9,10])
## Ruby
[1,2,3,4,5,6,7,8,9,10].delete_if {|i| i % 2 == 1}Pourquoi le Lambda-Calcul ?
La plupart des langages de programmation sont axée sur la gestion d'une machine, qu’elle soit un véritable ordinateur ou une machine virtuelle. La plupart des languages ont un caractère impératif, et le programmeur doit indiquer à la machine ce qu'elle doit faire étape-par-étape.
Les langages de programmation fonctionnels qui utilise le lambda-calcul ont une approche différente. Au lieu de dire à l'ordinateur comment résoudre un problème, ils permettent au programmeur de décrire quelle est la solution, et comment l'exécuter. Par exemple, il est plus facile de produire une nouvelle liste en utilisant une fonction comme
filtre
pour sélectionner les éléments que vous souhaitez conserver (ou, dans l'exemple de Ruby, en supprimant les éléments que vous voulez ignorer), que d'écrire une fonction C qui doit d'abord allouer de l'espace pour un résultat, puis placez chaque valeur sélectionnée dans la liste des résultats.Bien sûr, il n'est pas nécessaire d'utiliser un langage de programmation fonctionnel pour utiliser ces techniques. Et c'est parce que les idées du monde de la programmation fonctionnelle apparaissent dans les languages principaux, qu'il est plus important que jamais de comprendre ces techniques. Tom Christiansen le dit mieux :
Un programmeur qui n'a pas été exposé aux quatres styles de programmation : impératif, fonctionnel, object, logique, a un ou plusieurs angles morts conceptuels....
Plusieurs langages de programmation offre un mélange de styles. La plupart des langages orientés objet ont un noyau essentiel, où les classes, les objets et les méthodes fournissent une mince couche sur un language qui n'est guère plus qu'une version légèrement améliorée du C. De nombreux langages de programmation fonctionnels ont une mixité fonctionnelle, impérative, et des styles orientée objet, de sorte qu'il est difficile de les ranger dans une catégorie précise.
Haskell, par contre, est un langage purement fonctionnel qui se limite au style fonctionnel de la programmation. L'étude et l'utilisation d'Haskell rend facile de voir la puissance et les avantages du lambda-calcul et de la programmation fonctionnelle.
Le clivage linguistique
L'observation de Tom décrit quatre pierres angulaires de l'informatique : la programmation impérative, la programmation orienté objet, la programmation fonctionnelle, et la programmation logique. Ces quatre familles peuvent être regroupées en deux styles : mécanique (impératif et objet) et mathématiques (fonctionnelle et logique).
Les languages "mécaniques" se concentrent sur l'expression d'un programme comme une série d'instructions visant à être exécutés, les unes après les autres. La programmation orientée objet est plus puissante que la programmation impérative simple, car elle définit une série supplémentaire de règles qui permet au compilateur ou à l'interpréteur de faire d'avantage de travail de création pour composer un programme.
Les languages "mathématiques" commencent par un modèle théorique sur la façon de résoudre un problème, et propose une mise en œuvre concrète de ce modèle. Quatre exemples de cette approche sont données par : les expressions rationnelles, SQL, make et, bien sûr, Haskell.
Les expressions régulières utilisent la théorie des automates pour décrire un mécanisme d'appariement des chaînes de caractères. Il est beaucoup plus facile d'écrire une expression régulière comme
'^(NYC?|New York( City)?)$'
que d'écrire du code qui reconnait ces quatre chaines"NY"
,"New York"
,"New York"
et"New York City"
. En outre, on peut toujours employer une bonne d'expression régulière pour effectuer une recherche optimale, parcourant chaque caractère une fois seulement, tandis qu'avec une liste de chaines à rechercher, le procéder doit se répéter pour chacune d'elle et constitue un travail beaucoup plus grand et difficile.SQL utilise la théorie des ensembles et l'algèbre relationnelle pour rechercher des données dans une base de données. Une seule instruction SQL peut se référer à de nombreux tableaux, de nombreuses contraintes, et de nombreuses expressions calculées. Un moteur de base de données relationnelle classique utilise un optimiseur de requête pour déterminer si l'une des données nécessaires est indexé, et si oui, se réfère à cet indice pour réduire la quantité de travail nécessaire et retourner un résultat. L'alternative à l'utilisation de SQL et aux bases de données relationnelles consiste à écrire des pages de code pour effectuer toutes les opérations d'entré/sortie (I/O) nécessaire à la gestion des tableaux de données (et de leurs indices) sur le disque. Cela pourrait être faisable pour des déclarations SQL très simples, mais c'est certainement plus fastidieuse. Certaines instructions SQL complexes sont encore apréhendables et peuvent être programmées et comprises sans trop de difficulté, tandis que d'autres peuvent devenir des pages de code difficiles à écrire, source d'erreurs, difficiles à comprendre et à optimiser.
make
et les outils connexes commeant
,nant
etrake
, utilisent une série de recettes pour déterminer la façon de construire un projet logiciel. En interne,make
lit ces recettes et les interprète employant un peu de théorie des graphes pour modéliser les dépendances décrites par ces recettes. Quandmake
est utilisé avec un but précis, commemake test
,make docs
,make all
oumake debug
, il trace le graphe des dépendances de ce but en remontant en arrière l'ensemble des dépendances de tout ce qui doit être reconstruits et continue à construire seulement ce qui est absolument nécessaire. Ce processus est beaucoup plus fiable et facile à gérer qu'un script shell fabriqué à la main pour effectuer ce même travail. De plus,make
peut être exécuté dans un mode multiprocesseur, où de multiples appels indépendants peuvent s'exécuter simultanément afin d'accélérer le processus de construction, quelque chose qui est presque impossible à atteindre avec des scripts de construction codé à la main.Haskell s'inscrit également dans cette catégorie de langages de programmation, et le lambda-calcul en est le fondement.
make
, SQL et les expressions régulières, tous décrivent des languages de but limitées, convenables pour une résolution d'un type spécifique de problème dans le cadre d'un projet plus vaste. Haskell est un language à usage général, et il apporte la même sorte de moyens pour soulever de lourdes charges dans la conception et la construction d'applications et de grands systèmes.Haskell et l'avenir de la programmation
Le développement de logiciels est aujourd'hui à un point d'inflexion. Jusqu'à tout récemment, presque tous les logiciels ont été écrit pour fonctionner sur une seule machine avec un seul processeur. Aujourd'hui, de plus en plus de logiciels doivent être écrits pour s'accomoder au mode multiprocesseur, qu'il s'agisse de l'utilisation de plusieurs processeurs, de plusieurs nœuds sur un réseau, ou des deux. Pourtant, de nombreux langages et environnements travaillent aujourd'hui le mieux lorsqu'il sont utilisés sur une seule machine avec un seul processeur.
C'est une question ouverte, comment nous en tant que développeurs de logiciels devons-nous nous appuyer pour profiter de l'augmentation exponentielle de la puissance de calcul disponible.
Trois des idées les plus intéressantes sont disponibles dès maintenant, ou le seront bientôt, dans le compilateur ghc.
Un problème persistant dans le multitraitement de programmes tient dans gestion de l'état global partagé. Haskell permet d'éviter ces problèmes en posant toutes les valeurs immuables. En d'autres termes, les variables en Haskell ne varient pas. Chaque fois qu'une fonction retourne un résultat, il crée une nouvelle valeur, et laisse le garbage collector gérer l'allocation de mémoire et la destruction. Le résultat d'une fonction est renvoyé à l'appelant, et, par conséquent, il n'y a rien d'autre que l'état global, qui peut modifier le comportement d'un programme. Les programmes qui nécessitent un comportement statefull existent dans Haskell, mais ils sont strictement mis en quarantaine et peuvent être traitées séparément du reste du programme.
Les programmes Haskell employe le style "aucun partage" naturellement. C'est la même architecture qui est considéré comme une meilleure pratique pour écrire des applications web qui ont besoin dans leur developpement d'échelle d'intensifier l'utilisation de processeurs multiples ou de soutenir la réplication sur plusieurs nœuds du réseau.
Un autre problème dans le mode multiprocesseur est le conflits de ressources, où deux processus interfèrent l'un avec l'autre lorsqu'ils essaient de mettre à jour la même ressource partagée. Une solution consiste à utiliser des sémaphores et verrous de façon strictement coordonnée. Malheureusement, manipuler des processus et des verrous est très délicat, et il est très facile de se tromper. Une autre approche consiste à utiliser les transactions atomiques, où chaque émetteur réussit et écrit toutes ses modifications, ou échoue et n'écrit aucune d'entre elles.
Les moteurs de base de données ont utilisé des transactions pendant des décennies pour faire des mises à jour raisonnables multi-utilisateur. Jusqu'à récemment, ce mécanisme familier n'était pas disponible pour les développeurs de logiciels lors de l'écriture de logiciels en dehors de la portée d'une base de données. Ce modèle est disponible aujourd'hui dans le compilateur ghc, grâce à des logiciels de mémoire transactionnelle ou STM. Les bibliothèques soutenant STM apparaissent lentement pour d'autres langues aussi.
Un troisième problème porte sur l'utilisation efficace de plusieurs processeurs sur une même machine. Certains de ces problèmes "parallèles embarrassants " sont faciles à écrire en utilisant un ensemble d'instructions séquentielles à exécuter sur un seul processeur. Écrire du code qui répartit le travail sur plusieurs processeurs sur une même machine est beaucoup plus difficile.
Il y a du travail en cours pour remédier à cette situation dans ghc. Les données d'extension parallèle Haskell intègre une forme de calcul parallèle dans les programmes Haskell. Cette extension permet au programmeur de spécifier si les tableaux peuvent bénéficier d'opérations en parallèle qui sont alors répartis sur plusieurs processeurs dans une même machine. Cela permet aux programmeurs d'écrire un programme simple et normale Haskell, et d'identifier quelles sont les parties "parallèles embarrassantes" qui devront être parallélisée de manière transparente. "Data Parallel Haskell" est un travail en cours, et un domaine de recherche active par l'équipe de développement du ghc.
De toute évidence, Haskell est un langage important. Il mérite toute votre attention.
Partie 2: Fonctions Pures
Générale les langages de programmation objet peuvent être divisés en deux grandes catégories : Haskell et tout le reste.
Dans la partie 1, j'ai décrit comment tous les langages de programmation sont regroupés en quatre familles : impératif, objet, logique et fonctionnel. La plupart des langages de programmation commence par un noyau impératif auquel on ajoute sur le dessus des fonctionnalités orientées objet ou fonctionnelle. par exemple, C est un langage de programmation purement impératif. Les langages comme le C++, Java et C# s'appuyent sur C et enveloppe ce modèle impératif avec des objets. Perl, Python, Ruby ajoutent un peu de la programmation fonctionnelle dans le mélange.
Haskell est différent parce qu'il est un langage fonctionnel pure. Autrement dit, il ne supporte que le style de programmation fonctionnelle. Cette approche peu conventionnelle donne à Haskell la réputation d'être une langue difficile à apprendre et à utiliser, particulièrement parmi les programmeurs professionnels qui navigue négligemment autours entre les langues comme Java, Perl, Python, PHP et Ruby. La plupart des langues se concentrent sur les fonctionnalités à ajouter et à mélanger pour faire des développeurs plus productifs. Haskell prend la démarche inverse, en supprimant les particularités inutiles jusqu'à ce qu'il ne reste plus rien à emporter.
Pure contre Impures Fonctions
Les fonctions sont au cœur d'Haskell. Toutefois, les fonctions dans Haskell ne sont pas des fonctions analogues dans d'autres langues. Les fonctions Haskell sont pures, ce qui signifie qu'elles définissent une relation entre les entrées et les sorties, et n'ont pas d'effets secondaires (pas d'effets de bord). Ce comportement imite la notion de fonction que vous avez appris en algèbre il y a plusieurs années. Des opérations comme la racine carrée, le sinus, le cosinus, sont des fonctions pures ; la racine carrée de quatre vaudra toujours deux, peu importe comment, et où, ni quand il est calculé.
Les fonctions impures se référent à l'état externe, modifient leur comportement selon l'état externe, ou changent l'état externe. Comme fonctions C,
fopen()
,printf()
, etmalloc()
sont des exemples. Appelermalloc()
renvoie un pointeur vers un bloc de mémoire différents à chaque fois qu'elle est appelée, mais lorsque vous essayez d'attribuer plus que le système d'exploitation ne peut fournir,malloc()
échoue. De même, si vous essayez d'affecter un grand bloc de mémoire,malloc()
peut réussir, mais causer un dégrademment considérable des performances de la machine tout entière en changeant l'état du système d'exploitation. De toute évidence, les fonctions impures sont nécessaires, mais imprévisible, parce que leur comportement varie en fonction de comment, quand et où ils sont invoqués.Les deux types de fonctions sont nécessaires pour modéliser le monde réel. La conversion des températures Fahrenheit en degrés Celsius est une fonction pure ; par définition, 212 ° F doit toujours se convertir à 100 ° C. La conversion des dolars en livres au Royaume-Uni est une fonction impure qui est déterminée même par un grand nombre de variables externes, et il ne faut pas s'attendre à ce que la scène, la même opération à deux moments différents ou deux endroits différents puissent produire le même résultat.
Bien entendu, pour être un véritable langage de programmation général, Haskell doit soutenir à la fois les pures et les impures fonctions. Haskell dispose de fonctions impures, mais les isole grâce à l'utilisation des monades, qui sont en fait construit à partir des fonctions pures. Les monades constitues un sujet profond et intéressant, et sont discutées dans la partie n°3.
Programmer avec des fonctions pures
Parce que des fonctions pures se réfèrent uniquement à leurs entrées, elles sont déterministes, faciles à comprendre et faciles à prédire. Par exemple, une fonction comme :
square x = x * x
recevrez toujours un seul argument, et retourne la valeur de la multiplication de cet argument avec lui-même. Par conséquent, une expression telle que
square 2
revient toujours à4
. Une fonction qui calcule2 * 2
et écrit un message à la console ou dans un fichier journal est une fonction impure dont le comportement ne peut être prédit, le calcul peut réussir, mais la fonction peut échouer, en raison de l'opération de connexion et d'enregistrement. L'opération de connexion et d'enregistrement peut réussir la plupart du temps, mais il n'existe aucun moyen de prédire si cette fonction va tenter d'écrire dans un fichier fermée, d'écrire dans un fichier dont le système n'est plus disponible, d'écrire dans un système de fichiers plein, ou échouer sur un certain nombre d'autres échecs d'Entré/Sortie (Input/Output) (I/O)La clé de la compréhension des fonctions pure est de penser en termes de petites unités composables de code. Les opérations simples comme l'addition, la multiplication, le sinus, le cosinus, et la concatenation sont toutes de pures functions. Des fonctions pures peuvent être composées d'autres fonctions pures, comme ceux-ci ici :
f x = (3 * x * x) + (6 * x) + 9
De nombreuses fonctions dépendent de plus d'une valeur, et acceptent plusieurs arguments :
min a b = if a < b
then a
else bNotez que avec Haskell vous pouvez utiliser l'indentation pour signaler que l'expression est définie sur plusieurs lignes. C'est ce qu'on appelle la règle du hors-jeu, et elle est similaire aux règles de Python pour la portée de bloc. Contrairement à Python, la règle du hors-jeu d'Haskell est simplement une convention pour déduire l'emplacement des accolades et des points-virgules autour d'une série d'expressions et de déclarations. L'une ou l'autre forme est valable, mais la règle du hors-jeu est la plus commode et la plus commune des formes. (Le Haskell 98 Rapport définit l'interprétation exacte de la règle de mise en page.)
Certaines fonctions sont complexes et se référent à des sous-expressions plus impliquées. Voici une fonction qui calcule le coût d'un panier, où certains articles sont imposables, et d'autres ne le sont pas :
taxRate = 0.06
total cart = subtotal + tax
where
subtotal = sum cart
taxable = filter isTaxable cart
tax = (sum taxable) * taxRateCet exemple définit deux fonctions,
taxRate
, qui retourne une valeur constante, ettotal
, qui calcule le coût total de la liste des articles dans un panier. (Bien que la définition detaxRate
semble être la définition d'une variable, il est préférable de penser que c'est une fonction constante, une fonction qui ne prend aucun paramètre et renvoie toujours la même valeur.) La définition detotal
est très expressive, et souligne l'intention de la fonction, en isolant et en nommant d'importantes sous-expressions dans le calcul. (total
se réfère également à la fonctionisTaxable
non présentés ici).La clause
where
danstotal
définit significativement les sous-expressions qui sont utilisés pour évaluer l'expression primaire,subtotal + tax
. Lesubtotal
, comme on peut s'y attendre, n'est que la somme de tous les articles dans le panier. La définition detax
se rapporte àtaxable
, qui est le sous-ensemble de tous les articles dans le panier qui sont imposables (plus précisément, l'ensemble des articles dans le panier oùisTaxable
est vrai pour ce point). Le calcul de la taxe consiste à sommer le sous-ensembles des articles imposables, et à multiplier cette somme par le taux d'imposition. Les deux fonctions d'utilitésum
etfilter
savent faire ce que leur nom indique et sont prédéfinies dans Prélude, la bibliothèque standard d'Haskell de fonctions utiles.On pourrait aussi définir
total
en évitant les sous-expressions dans la clausewhere
, et rédigeant le calcul entier dans une forme entièrement étendue. Cette version est tout aussi correct, mais beaucoup plus difficile à comprendre pour les gens :
total cart = (sum cart) +
(sum (filter isTaxable cart)) * taxRateLe compilateur ne se soucie pas comment vous écrivez ces fonctions, parce qu'ils sont des expressions équivalentes de la même idée. En fait, on permet au compilateur de récrire la première définition dans le deuxième. Cependant, l'utilisation des outils comme les clauses where et les bibliothèques de fonctions utiles et réutilisables peut aider à rendre les fonctions concises et expressives, et plus facile à lire pour les gens.
Programmation Fonctionnelle Réelle-du monde
Haskell a des racines profondes dans le monde des mathématiques, et il est facile de voir comment il peut être utilisé pour définir des fonctions mathématiques simples. Toutefois, les problèmes du monde réel ont très peu de chose en commun avec de simples fonctions mathématiques.
Ou bien ils faire?
Envisagons un programme qui doit trouver les 10 mots les plus utilisés dans un document. Supposons pour le moment que le document est disponible en tant que valeur de chaîne. (Lecture d'un fichier à partir du disque ou de l'entrée standard, il faudrait utiliser la monade I/O, qui sera discuté dans la partie n°3.)
Heureusement, il existe une variété de fonctions pré-définies dans Prélude qui peuvent faire ce simple travail. La première étape consiste à convertir un document dans une liste de mots. C'est précisément l'opération effectuée par la fonction
words
dans Prélude:
top10 doc = ....
where
listOfWords = words doc
...Ensuite, la liste des mots doit être normalisée afin que les variations de casse ne puissent pas en infléchir les résultats. Une façon de faire consiste à convertir chaque mot en minuscules. Toutefois, il n'y a pas de fonction
lowercase
prédéfinie qui convertit tous les caractères majuscules d'une chaîne en son équivalent en minuscules. Mais il y a une fonctiontoLower
dans la bibliothèqueData.Char
qui convertie un caractère en minuscule. Avec l'utilisation demap
, nous pouvons appliquer cette transformation à chaque caractère dans une chaîne :
import Data.Char
lowercase str = map toLower str
Ou, plus consisement :
import Data.Char
lowercase = map toLower
Le compilateur Haskell peut en déduire que
lowercase
doit prendre une chaîne (une liste de caractères) comme argument, parce qu'il sait quemap
prend deux arguments (une fonction et une liste), ettoLower
convertit un caractère dans une autre. Commemap toLower
a besoin d'un argument,lowercase
doit avoir besoin du même argument.La mise à jour de la définition du
top10
est facile. Au lieu de diviser le document en une liste de mots, nous pouvons diviser la version minuscule du document dans une liste de mots en minuscules :
import Data.Char
lowercase = map toLower
top10 doc = ....
where
listOfWords = words (lowercase doc)
...Ensuite, les mots doivent être triés et groupés. Ces opérations peuvent être effectuées par les fonctions
sort
etgroup
dans la bibliothèqueData.List
. Le sort de fonction fait ce que vous attendez. La fonctiongroup
prend une liste de valeurs et retourne une liste de listes imbriquées, où chaque liste imbriquée contient voisine des valeurs égales. Par exemple :
$ ghci
Prelude> :module + Data.List
Prelude Data.List> group [1,3,3,1]
[[1],[3,3],[1]]
Prelude Data.List> group (sort [1,3,3,1])
[[1,1],[3,3]]L'ajout de cette fonctionnalité pour
top10
est simple:
import Data.List
top10 doc = ....
where
listOfWords = words (lowercase doc)
wordGroups = group (sort listOfWords)
...Obtenir la liste des mots ordonnée selon leur fréquence implique un peu de manipulations sur la liste des groupes de mots. Chaque groupe de mots est une liste d'une ou plusieurs instances d'un seul mot. Un moyen facile d'ordonner par fréquence est de convertir ces listes en une paire de valeurs, où le premier élément est le nombre de mots (fréquence) et le second élément est le mot lui-même. Une fonction simple peut effectuer cette action sur un groupe de mots :
makePair l = (length l, head l)
La conversion d'une liste de groupes de mots se fait simplement en utilisant
map
et en l'appliquant sur la liste complète :
-- convert [["a", "a"], ["b"], ...]
-- into [(2, "a"), (1, "b"), ...]
map makePair wordGroupsToutefois,
makePair
est profondément lié au problème que nous essayons de résoudre et n'est donc pas très générique. Au lieu de créer un nom pour cette fonction jetables, on peut utiliser une expression lambda à la place :
-- same as above
map (\l -> (length l, head l)) wordGroupsLa fonction lambda ou une fonction anonyme commence par un caractère
\
. Les paramètres nommés se situent à la gauche de la flèche et le corps de la fonction se situe à droite de la flèche.Générer la liste des mots et leur fréquence n'est qu'une partie de la solution. Ensuite, les mots doivent être triés par ordre décroissant de leur fréquence :
reverse (sort (map (\l -> (length l, head l)) wordGroups))
Malheureusement, les emboitements de parenthèses compliquent la structure de cette expression. Nous pouvons utiliser l'opérateur $ pour nettoyer l'expression comme cela :
reverse $ sort $ map (\l -> (length l, head l)) wordGroups
L'opérateur $ est un peu de sucre syntaxique. Il prend tout à droit et le traite comme une seule expression, puis utilise ce côté droit comme le dernier paramètre de l'expression présente sur le côté gauche. Par conséquent, ces deux formes sont vues en réalité par le compilateur comme la même expression.
La mise à jour de la fonction
top10
ressemble à ceci :
top10 doc = ....
where
listOfWords = words (lowercase doc)
wordGroups = group (sort listOfWords)
frequencies = reverse
$ sort
$ map (\l -> (length l, head l))
wordGroups
...Ensuite, la liste des paires (fréquence, mot) doivent être réduit à une liste de mots. Les paires sont une structure de données si utile dans les programmes Haskell que Prélude comprend deux fonctions standart pour obtenir leur contenu :
fst
retourne le premier élément d'une paire, etsnd
renvoie le deuxième élément d'une paire. Pour obtenir les mots de ces couples, nous avons besoin d'utilisersnd
. Pour convertir la liste de paires dans une liste de mots, nous avons juste besoin d'utilisermap
:
top10 doc = ....
where
listOfWords = words (lowercase doc)
wordGroups = group (sort listOfWords)
wordPairs = reverse
$ sort
$ map (\l -> (length l, head l))
wordGroups
wordsByFreq = map snd wordPairsEnfin, l'objectif de la fonction
top10
est de trouver les dix premiers mots les plus fréquentes dans un document. Depuis quewordsByFreq
ordonne du plus fréquent au moinss fréquent, tout ce que nous devons faire est de trouver les dix premiers éléments de la liste. Cela est assuré par la fonction standarttake
présente dans Prélude.take
a deux paramètres : la taille désirée et une liste de valeurs, elle retourne une liste ne dépassant pas la taille désirée.Avec cette dernière pièce du puzzle, voici la définition complète de la fonction
top10
:
import Data.Char
import Data.List
lowercase = map toLower
top10 doc = take 10 wordsByFreq
where
listOfWords = words (lowercase doc)
wordGroups = group (sort listOfWords)
wordPairs = reverse
$ sort
$ map (\l -> (length l, head l))
wordGroups
wordsByFreq = map snd wordPairsLe chargement du texte intégral de la RFC 822 en une chaines de caractères nommée rfc822, et le lancement de cette chaine à travers la fonction top10 produit ce résultat :
$ ghci top10.hs
___ ___ _
/ _ \ /\ /\/ __(_)
/ /_\// /_/ / / | | GHC Interactive, version 6.6, for Haskell 98.
/ /_\\/ __ / /___| | http://www.haskell.org/ghc/
\____/\/ /_/\____/|_| Type :? for help.
Loading package base ... linking ... done.
[1 of 1] Compiling Main ( top10.hs, interpreted )
Ok, modules loaded: Main.
*Main> rfc822 <- readFile "rfc0822.txt"
*Main> top10 rfc822
["the","of","to","a","is","and","be",";","for","in"]
*Main>En utilisant cette approche pour définir une fonction comme
top10
, on pourrait écrire une série de fonctions similaires. Par exemple, une version qui enlève les caractères de ponctuation accolés sur chaque mot. Ou une version qui génère une liste unique de mots, pour être utilisée par un correcteur orthographique. Ou une version qui renvoie à la fois des mots et des fréquences. Ou une version qui ne tient pas compte des mots tel que "à", "de", "du".... L'écriture d'une série de tels fonctions identifiera probablement des composantes communes, comme une fonction qui convertit une liste de mots en une liste de paires (fréquence, mot), ou une fonction qui convertit une liste de paires en une liste de mots.La série est longue, et je vous encourage à jouer avec cette petite fonction simple pour explorer Haskell plus loin.
Conclusion
De nombreux programmeurs expérimentés resentent une sorte de choc culturel lors de la première approche d'Haskell. Comment peut-on programmer sans variables affectable ? Où sont les objets ? Les hiérarchies de classes ? Les modèles de conception ? Comment pouvez-vous déboguer un programme sans émaille états d'impression à propos? Si les fonctions pures peuvent modèliser simplement des concepts mathématiques, peuvent-ils aussi modéliser des problèmes de programmation réel ?
Dans cet article, j'ai montré comment développer un code réel en utilisant seulement des fonctions pures. Cette approche est à la fois puissante et différente de ce que la plupart des programmeurs utilisent aujourd'hui. Haskell vous encourage à réfléchir en termes de petites fonctions puissantes qui font une chose et le font bien. Pour vous aider dans vos efforts, les bibliothèques standard fournissent une multitude de fonctions pour l'étude, la réutilisation, et se combiner dans des fonctions nouvelles et intéressantes qui vous permettent de mettre l'accent sur les problèmes que vous essayez de résoudre, et non sur la façon de rendre votre problème conformer à une API ou à un modèle de conception.
La programmation fonctionnelle en Haskell est très riche, est constitue un sujet très profond, qui ne peuvt pas être expliqué en détail dans un seul article. Visitez le wiki d'Haskell pour découvrir quelques-uns des sujets fort intéressants, comme :
Partie 3: Les monades
Dans la partie 1, j'ai décrit comment les langages de programmation sont regroupés en deux familles, mécanique et mathématique. Les programmeurs aussi se répartissent en deux catégories générales, ceux qui pensent mécaniquement sur ce qu'est un ordinateur et comment il exécute un programme, et ceux qui pensent mathématique ce que signifie un programme. Certains programmeurs sont doués et font le pont entre ces deux mondes si nécessaire.
Dans la partie 2 , j'ai décrit comment Haskell est un langage de programmation purement fonctionnelle et comment il utilise les fonctions pure pour exprimer des programmes. Malheureusement, les fonctions pures limitent, parce qu'elles interdisent les effets secondaires (effet de bord) et donc aussi les entrée-sorties de base.
Cet article décrit les monades, un mécanisme d'Haskell pour structurer le calcul, permettre des effets secondaires (des effets de bord) et communiquer avec le monde extérieur.
Le clivage linguistique revisité
Haskell est issu d'une longue et riche histoire de la recherche en mathématiques et en programmation fonctionnelle. Naturellement, de nombreux programmeurs à l'esprit mathématique sont très à l'aise avec Haskell, tandis que ceux à l'esprit mécanique sont généralement brouillés avec cette langue et les concepts mathématiques dont elle s'inspire.
Aucun concept ne stupéfie plus que la monade.
Les monades viennent d'une branche des mathématiques appelée la théorie des catégories (Pour en savoir plus sur la théorie des catégories, voir sur Wikipédia et Haskell.org). Malheureusement, la théorie des catégories est une branche des mathématiques obscure que la plupart n'ont jamais vu, même lorsque l'on étudie les fondements mathématiques de l'informatique.
Les nouvelles ne sont pas toutes mauvaises. Par exemple, SQL est construit sur une base mathématique de l'algèbre relationnelle. Un programmeur peut penser une requête SQL en termes de ce que les opérateurs relationnels veulent dire ou en termes de ce que le moteur de base de données fait. Que ce soit un programmeur incliné mathématiquement ou mécaniquement, il peut toujours formuler des requêtes SQL afin d'examiner des données dans une base de données.
De même, les programmeurs incliné mécaniquement n'ont pas besoin d'apprendre la théorie des catégories pour comprendre ce que signifie les monades et afin de comprendre comment elles fonctionnent. Mais avant que je puisse expliquer ce que font les monades et comment elles travaillent, j'ai besoin d'expliquer l'un des éléments les plus novateurs d'Haskell, son système de types.
Le système de Type
Parce que Haskell est si profondément liée aux mathématiques, ses concepteurs et ses praticiens sont intéressés à comprendre ce que signifie un programme. Le compilateur d'Haskell essaye de comprendre ce que votre programme signifie grâce à une analyse de type et de l'inférence de type.
Les l angages tels que C, Java et C# offrent une forme simple de typage statique. Le typage statique dans ces langues consiste à ajouter des annotations à chaque variable, fonction et déclaration de méthode pour indiquer au compilateur comment allouer de la mémoire et générer du code. Malheureusement, ce genre de frappe est très bavard et très vulnérable aux erreurs. Dans ces langues, le système de type peut également être subverti, par conversion brute de type ou en ignorant les erreurs de type par le compilateur. En conséquence, il n'est pas difficile d'écrire du code avec des erreurs de type, ce qui cause qu'un programme tombe en panne.
Les l angages comme Perl, Python, et Ruby offrent une certaine forme de typage dynamique. Dans ces langues, peu ou pas de vérification de type n'est faite lors de la compilation d'un programme. Au lieu de cela, les questions de type sont différés jusqu'à l'exécution, et un programme est supposé avoir un sens et être bien typé. Cela signifie que des annotations de type sont inutiles, et des programmes dans ces langues sont généralement plus concis que leurs homologues dans les langages C et similaires. Des erreurs de type, si elles se produisent, générent des exceptions à l'exécution, et peut entraîner le crash d'un programme de crash, ou elles peuvent être ignorés, conduisant à un comportement étrange et inattendue d'exécution.
Haskell a une approche très différente. Il commence par une analyse de type avec un correcteur de type, à comprendre votre programme au moment de la compilation. Le vérificateur de type interdit strictement la conversion brute de type et ne permet pas aux erreurs de type d'être ignorés. Parce que les types sont vérifiées à la compilation, et qu'il ne peut échapper au vérificateur de type, Haskell est souvent décrit comme la fois typé statiquement et fortement typé.
Tout programme Haskell avec une erreur de type n'est tout simplement pas compilable. Les programmes Haskell qui se compile sont exempts d'erreurs de type, et cette garantie élimine toute une classe d'accidents et de comportements inattendus d'exécution. (Les erreurs de type sont simplement un type de problème qui se glissent dans un programme. Il y a encore beaucoup de manières pour un programme Haskell de se compiler et de se mal conduire à l'exécution.)
En outre, Haskell utilise l'inférence de type pour déduire les types des fonctions et valeurs, éliminant le besoin d'annotations de type qui sont verbeuses dans la plupart des cas. Ces caractéristiques se combinent pour fournir plus de sécurité que le typage statique des langage C et similaires, et un code au moins aussi concis que ceux trouvés dans les languages dynamiques.
Par exemple, voici la définition de
length
, une fonction qui prend une liste et retourne le nombre de valeurs qu'il contient :
length [] = 0
length (x:xs) = 1 + length xsVoici une annotation de type que le compilateur peut déduire de cette fonction :
length :: [a] -> Int
Cette annotation peut être lu comme suit :
- Le
::
sépare le symbole qui est situé à sa gauche (length
) de sa signature de type qui est situé à sa droite ([a] -> Int
).[a] -> Int
décrit une fonction qui accepte une valeur de type[a]
et retourne une valeur de typeInt
.a
est une variable de type qui peut correspondre à n'importe quel type.[a]
est une liste de tout type de valeur.Int
est un entier machine natif.Le compilateur peut déduire ce type parce que :
- La valeur du scénario de base est
0
, unInt
.- La valeur du cas normale vaut
1 +
unInt
, qui doit également être unInt
.length
n'examine pas le contenu de la liste, donc le type d'éléments contenus dans la liste ne doit pas importer.Bien que les annotations de type ne sont pas strictement nécessaire, c'est une bonne idée de les inclure directement au-dessus une définition de fonction. Cela permet au compilateur de vérifier que votre idée du type de la fonction correspond au type réel de la fonction. Plus important encore, l'annotation de type agit également à titre de documentation pour d'autres qui lisent votre code. Une meilleure façon de définir length serait :
length :: [a] -> Int
length [] = 0
length (x:xs) = 1 + length xsVoici une définition de la fonction d'ordre supérieur
map
, qui applique une fonction à chaque élément d'une liste pour créer une nouvelle liste :
map :: (a -> b) -> [a] -> [b]
map f (x:xs) = (fx):(map f xs)Cette annotation de type signifie ici ce qui suit :
map
est une fonction qui prend deux valeurs ((a -> b)
et[a]
), et retourne une troisième valeur ([b]
).- La première valeur est une fonction qui prend une valeur d'un type quelconque
a
, et retourne une valeur d'un type quelconqueb
.- La deuxième valeur est une liste de type
a
.- La valeur du résultat est une liste de type
b
.- Les types de
a
etb
peuvent être de deux types, mais le typea
dans le premier argument doit correspondre au typea
, dans le deuxième argument, tout comme le typeb
dans le premier argument doit correspondre au typeb
dans le résultat.Voici un exemple d'une fonction utilisant
map
:
ft = map length (lines t)
Le compilateur peut déduire le type de cette fonction :
f :: String -> [Int]
C'est parce que :
lines
accepte un argument de chaîne et retourne une liste de chaînes (c.-à-String -> [String]
).length
accepte une liste de tout type et retourne unInt
(ie[a] -> Int
).- Dans ce cas,
map
prend la fonctionlength
de convertir une liste de chaînes (une liste de listes de caractères) dans une liste de numéros.Par conséquent, le type de
f
doit êtreString -> [Int]
(parfois formulée comme[Char] -> [Int]
).Classes de Type
Jusqu'à présent, l'inférence de type semble utile pour déduire le type d'une fonction lorsque des types spécifiques ou des types génériques sont utilisés. Toutefois, cela laisse un vide énorme dans les fonctions intermédiaires où n'importe quel type d'un groupe de types liés peut être utilisés.
C'est là que les classes de type entrent en jeu. Une classe de type unifie un ensemble de types connexes, interchangeables. Voici un exemple typique, la class de type
Num
qui définit une série d'opérations qui peuvent être utilisés avec tout type de nombre :
class Num a where
(+) :: a -> a -> a
(-) :: a -> a -> a
(*) :: a -> a -> a
...Cette déclaration de classe signifie que n'importe quel type
a
qui est un membre de la classe de typeNum
doit mettre en œuvre les trois opérations+
,-
, et*
(et quelques autres qui ne figurent pas ici). En outre, la mise en œuvre de+
pour tout type qui implémente cette interface doit prendre deux valeurs et renvoyer une valeur. Les trois valeurs doivent également être du même type numérique.Une déclarations de classe de type définit l'interface qu'un type doit mettre en œuvre pour être membre de cette classe de type. Une déclaration d'instance lie ensemble cette interface avec une mise en oeuvre spécifique pour un type simple. Voici les déclarations d'instance qui précisent que
Int
etDouble
sont tous deux membres de la classe de typeNum
:
-- low level multiplication of 2 integers
mulInt :: Int -> Int -> Int
mulInt a b = ...
instance Num Int where
(*) = mulInt
...
-- low level multiplication of 2 doubles
mulDouble :: Double -> Double -> Double
mulDouble a b = ...
instance Num Double where
(*) = mulDouble
...Le vérificateur de type sait comment en déduire quand un type de classe est nécessaire pour décrire le type d'une fonction. Voici la simple fonction de l'article précédent que le nombre de places :
square x = x * x
L'inferencer de type déduit le type suivant pour cette fonction :
square :: (Num a) => a -> a
Cette signature de type est un peu différente, parce qu'il inclut une contrainte supplémentaire, à savoir l'implication
(Num a)
à la gauche du symbole=>
. Cela signifie que la fonction square est une fonction qui prend une valeur de typea
, et retourne une valeur du même typea
, et ce type doit être également membre de la classe typeNum
.Par exemple, l'expression
square 3
est valable, parce que3
est de typeInt
etInt
est un membre de la classe de typeNum
. Évaluer cette expression entraine le compilateur Haskell à regarder sous les couvertures pour voir comment la multiplication est définie pour les valeursInt
et de remplacer l'invocation de * par la mise en œuvre effective de la multiplication desInt
, c'est à dire par la fonctionmulInt
. L'évaluation de l'expressionsquare 2.5
est valable également, parce que la valeur2.5
est de typeDouble
etDouble
est un membre de la classe de typeNum
. Dans ce cas, le compilateur appelle la version de*
pour le typeDouble
, c'est à dire la fonction nomméemulDouble
dans cet exemple.L'expression
square "this"
n'est pas valide parce que square est définie uniquement sur les nombres, et la valeur"this"
est une chaîne, et les chaînes ne sont pas des numéros. Tenter d'évaluer cette expression dans l'interpréteur produit l'erreur suivante :
$ ghci
...
Prelude> let square x = x * x
Prelude> :t square
square :: (Num a) => a -> a
Prelude> square "this"
<interactive>:1:0:
No instance for (Num [Char])
arising from use of `square' at <interactive>:1:0-12
Possible fix: add an instance declaration for (Num [Char])
In the expression: square "this"
In the definition of `it': it = square "this"Voici la preuve que cette définition de square fonctionne sur différents types de nombres :
Prelude> square 3
9
Prelude> square 2.5
6.25Monades
Maintenant que vous avez vu comment les classes de types regroupent une série de types, il est temps de revenir sur la question des monades. Les classes de types sont importantes, parce que, au niveau le plus fondamental de l'implémentation, les monades ne sont que des conteneurs qui sont des instances de la classe de type Monad :
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
(>>) :: m a -> m b -> m b
return :: a -> m a
fail :: String -> m aVoici ce que cette déclaration de classe de type signifie :
- Un type
m
est une monade si elle met en œuvre quatre opérations : l'opération de liaisonbind
(opérateur>>=
),then
(opérateur>>
),return
, etfail
.- Les monades sont des types d'ordre supérieur. Une monade
m
est quelque chose qui agit comme un conteneur sur un autre type (exprimés parm a
oum b
dans cette définition).- L'opération de liaison bind prend un conteneur (
m a
), et une fonction (a -> m b
) et retourne un nouveau conteneur (m b
). Cette fonction doit accepter la valeur du premier conteneur et produire le deuxième conteneur.- L'opération
then
travaille commebind
, à l'exception que la valeur dans le premier conteneur est ignoré, et la deuxième fonction est remplacée par un simple conteneur.- L'opération
return
prend une valeur simple et la retourne dans un conteneur.- L'opération
fail
prend une chaîne et renvoie un conteneur. (Cette opération peut être employée pour "échouer vite", ainsi une fois que l'on rencontre un échec, toutes les opérations suivantes sont sautées.)Ces idées viennent de la théorie des catégories, qui définit également quelques propriétés importantes que les opérateurs monadiques doivent fournir :
- L'expression
return a >>= f
est équivalent àf a
.- L'expression
m >>= return
, est équivalent àm
. (Cela signifie que la fonction return utilisée toute seul est une no-op. return existe pour injecter des valeurs dans un conteneur.)- L'expression
(m >>= f) >>= g
, oùm
est un conteneur etf
etg
sont des fonctions, est équivalente à l'expressionm >>= (\x -> fx >>= g)
. Autrement dit, l'opération de liaison fonctionne toujours de gauche à droite.Ces définitions sont tout à fait formelle et peuvent être quelque peu déroutante les premières fois que vous les lisez, mais l'objectif ici est de définir une série d'opérations mécaniques simples pour le raccordement des conteneurs ensemble. Les monades sont un moyen d'enchaîner une séquence d'opérations d'une manière qui fonctionne en utilisant seulement des fonctions pures. Et parce qu'elles sont définis en termes de fonctions pure, le compilateur Haskell peut les comprendre et traiter leur travaux pour vous.
Penser à ces opérations dans l'abstrait peut être un peu difficile. Voici un exemple simple. Ces deux fonctions fonctionnent dans la monade
IO
, le mécanisme de Haskell utilisé pour communiquer avec le monde extérieur.
getLine :: IO String
putStrLn :: String -> IO ()Parce que toutes les opérations d'Entré/Sortie sont monadique, la monade
IO
agit comme un conteneur qui isole les effets secondaires externes dans un programme Haskell. La fonctiongetLine
lit une ligne d'entrée de la console et renvoie cette chaîne à l'intérieur du containerIO
. La fonctionputStrLn
accepte une valeur de chaîne, imprime cette chaîne à la console (avec un saut de ligne), et retourne une valeur vide, également contenu dans la monadeIO
.Voici comment ces fonctions sont reliées entre elles, en utilisant les opérateurs de monades :
echo :: IO ()
echo = getLine >>= putStrLnDans cet exemple, l'action monadique
getLine
est évaluée et produit unIO String
en résultat. La fonction bind (>>=
) obtient ce résultat, extraits la chaîne de son conteneurIO
, et l'envoie à l'action monadiqueputStrLn
. Enfin, l'actionputStrLn
consomme cette chaîne simple, il l'envoie à la console, et retourne la valeur vide dans le conteneurIO
(de typeIO ()
). Ce résultat final est renvoyé par la fonctionecho
, aussiecho
doit avoir le typeIO ()
.Utiliser les opérateurs monades de cette façon peut être puissant, mais c'est très encombrant. Heureusement, Haskell propose un autre moyen de décrire les opérations monadiques, la notation "monadic do" . Voici une façon équivalente d'écrire la fonction
echo
:
echo :: IO ()
echo = do str <- getLine
putStrLn strIci, une fonction monadique est une série de déclarations, alignés selon la règle de mise en page. La fonction
getLine
retourne une valeur, de sorte que la valeur peut être capturé dans le symbolestr
et utilisées plus tard dans cette fonction. (Rappelez-vous,str
n'est pas une variable, parce que sa valeur ne peut pas changer.) Parce que cette valeur est extraite à partir du conteneur monadique, la valeur destr
ici est de typeString
, et pasIO String
. Par conséquent, cette valeur peut être passée à la fonctionputStrLn
, qui s'attend à uneString
comme premier argument.Parce que ces actions sont effectuées de gauche à droite (ou de haut en bas dans la notation "monadic do"), les monades exécutent une liste ordonnée d'instructions que constituent leurs déclarations. Avec des fonctions pures, les sous-expressions peuvent être évalués dans n'importe quel ordre, sans en changer le sens. Avec des fonctions monadique, l'ordre d'exécution est très important. Par exemple, il est inutile d'évaluer
putStrLn
dans cette fonction avant de recevoir son entrée à partir degetLine
.Conclusion
La monade
IO
(la monade d'Entré/Sortie) n'est pas la seul monade en Haskell, mais elle est l'une des plus fréquemment utilisés. D'autres monades proposent différents types de conteneurs et de structures de code. Par exemple, les listes sont de simples structures de données, mais elles peuvent aussi être utilisés comme des monades. La bibliothèque standard de GHC contient des séries de monades utiles qui sont simples, génériques, et utile dans une variété de domaines. L'une de ces bibliothèque estParsec
, une bibliothèque très pratique pour écrire des analyseurs syntaxiques.Un grand nombre de nouveaux travaux passionnants en Haskell sont fait en utilisant des monades. Par exemple, la mémoire transactionnelle et l'imbrication de parallélisme de données sont deux techniques utiles qui aident lors de la distribution d'un programme Haskell sur plusieurs processeurs. Les parties du programme qui emploient ces particularités sont strictement isolées des parties du programme qui sont purement fonctionnel ou traitant avec
IO
. Cela rend la tâche plus facile pour le compilateur d'isoler les parties d'un programme qui sont transactionnel ou en parallèle, des parties qui ne le sont pas. En outre, le système de type applique cette séparation et met en lumière les bugs dans le code où les monades sont mal utilisés et ne font aucun sens.Toutes ces structures ont une chose en commun : elles sont des types de conteneurs qui sont déclarés comme des instances de la classe de type
Monad
. Chaque type de monade définit sa version des opérations de base :bind
(>>=
),then
(>>
),return
, etfail
. Le compilateur Haskell peut alors composer une séquence d'actions monadiques en une seule grosse action monadique. Le compilateur remplace également ces opérations génériques par des implémentations spécifiques nécessaires pour une monade donnée, de même qu'il remplace la multiplication générique numérique avec l'opération spécifique nécessaire pour multiplier des valeursInt
ouDouble
. En outre, parce que toutes les monades utilisent les mêmes opérateurs de base, une actions monadique compliquée peut être exprimée par une simple notation "monadic do", ce qui rend ces opérations plus faciles à comprendre.