J'ai entendu parler d'Haskell pour la première fois à l'université. J'entendais que c'était un langage très élégant, amusant et bien plus avancé qu'OCaml. Mais OCaml me donnait déjà bien du mal, et c'était ce dernier qui était demandé pour les examens et les projets de programmation. Je n'étais donc pas prêt à apprendre Haskell à ce moment.

Plus tard, par curiosité, j'ai regardé quelques projets écrits en Haskell. Je suis alors tombé sur xmonad, un gestionnaire de fenêtres spartiate, mais avec des idées intéressantes, comme l'agencement automatique. J'ai aussi découvert la bibliothèque Parsec, qui permet d'écrire des parseurs très simplement, et dont la lecture du code m'a convaincu que la notion de fonction était une abstraction vraiment puissante.

Lors de mon premier contact avec du code Haskell, je n'ai pas été très enthousiaste :

  • Cela ne ressemblait à aucun langage que je connaissais, on aurait dit une peu de l'anglais mathématique.
  • Après quelques tentatives de modification du code, je me faisais insulter par le compilateur...

Mais ma curiosité était piquée. J'ai toujours été intrigué par les langages que je ne comprends pas. De plus, il y avait une promesse de concision, vu qu'il me semblait que l'on puisse faire beaucoup avec peu de lignes.

Qu'est-ce que Haskell ?

Haskell est d'abord un langage de comité, composé de mathématiciens et d'informaticiens désireux de faire des expériences en programmation fonctionnelle, et aussi de créer un langage utilisable pour écrire des applications. Le langage étant en quelque sorte expérimental, il est en perpétuelle évolution. Impossible donc d'écrire une documentation ou un livre sans être instantanément démodé. Haskell est donc stabilisé en 1998 en tant que Haskell 98. Ce qui met fin également au comité, mais pas à la vie du langage, bien au contraire.

Quelques traits caractéristiques du langage :

  • Fonctionnel : l'abstraction de base est la fonction.
  • Pur : pas d'effets de bord.
  • Paresseux (lazy) : l'évaluation se fait lorsque l'on a besoin d'une valeur, à la différence de la plupart des autres langages, où les arguments d'une fonction sont évalués avant l'appel de celle-ci. Ceci permet par exemple de manipuler des structures de données de taille infinie.

Ces caractéristiques en font un langage singulier, pas forcément facile à appréhender. Le coté fonctionnel pur en font un langage de choix pour la programmation parallèle, bien qu'il n'ai pas été conçu pour cela au départ. Certaines start-up choisissent également ce langage en misant sur un avantage décisif par rapport à la concurrence. Ainsi, Tsurucapital annonce sur sa page d'accueil:

We use the Haskell programming language almost exclusively since we believe it gives the best combination of high-level cleanliness, execution safety and high performance.

Haskell est bien entendu aussi utilisé par des mathématiciens, mais excelle dans des taches plus communes, comme l'écriture de DSL, ou plus récemment dans la programmation web (Yesod, Snap)

Environnement de développement

J'utilise ghc, le compilateur Haskell le plus utilisé et les plus avancé à l'heure actuelle. Ghci est livré avec ghci, un interpréteur Haskell bâti autour de ghc. Il existe très certainement un package pour votre OS.

Si vous le souhaitez, vous pouvez essayer d'autres environnements:

  • Haskell Platform qui est constitué de ghc et d'un ensemble de bibliothèques d'usage fréquent.
  • Hugs est un environnement qui est aussi populaire dans le monde des débutants en Haskell.

Après avoir installé ghc (ou Haskell Platform), vous pourrez exécuter un fichier en utilisant la commande runhaskell, ou bien le charger dans d'interpréteur en utilisant la commande ghci.

Une fois dans l'interpréteur, le Prelude (ensemble des bibliothèques de base) est chargé et vous vous retrouvez en mode interactif.

Fonctions

En Haskell, une fonction prend en entrée des valeurs, et retourne une
valeur (qui peut bien entendu être une fonction). Une fonction ne peut pas provoquer d'effets de bord. Par conséquent, le résultat d'une fonction ne dépend que de ses paramètres. En d'autres termes, la notion d'état n'existe pas. La déclaration d'une fonction consiste en une équation, signifiant que la partie à gauche du signe = est équivalent à la partie droite.

La commande :t vous sera certainement utile dans l'interpréteur. Elle permet d'afficher le type d'une expression. Par exemple, si je veux connaître le type de la fonction length :

Cela se lit comme suit: "Quel que soit le type a, la fonction length prend en argument une liste de a, et retourne un entier".

Transformer les éléments d'une liste

Une opération très fréquente lorsque l'on programme est de parcourir une liste en appliquant une fonction à chacun de ses éléments. C'est ce que permet la fonction map.

La description du type nous montre que les arguments sont :

  1. Une fonction qui, pour toute valeur de type a, retourne une valeur de type b
  2. Une liste de valeurs de type a

La valeur de retour est une liste de valeurs de type b.

Dans notre exemple où l'on multiplie par deux les éléments d'une liste d'entiers, les types a et b se trouvent être égaux.

Filtrer une liste

Un autre traitement très fréquent en programmation est de filtrer les éléments d'une liste selon un prédicat. C'est ce que propose la fonction filter.

La lecture du type nous informe que les arguments sont :

  1. Un prédicat, c'est à dire une fonction à un argument qui retourne un booléen
  2. Une liste d'éléments

On constate aussi que les éléments de la liste résultat sont de même types que les éléments de la liste en paramètre. Ce qui est conforme à ce que nous attendons : la liste est simplement filtrée, les valeurs ne sont pas transformées.

Fonctions anonymes

Il est possible de déclarer une fonction sans nom. Par exemple, pour une fonction qui double la valeur de son argument, on peut écrire \ x -> 2 * x. Ce qui est pratique lorsque l'on a besoin d'une fonction, mais que l'on ne veut pas prendre la peine de la définir, parce qu'elle est très petite ou triviale.

A ceux qui se poseraient la question, le caractère \ a été choisi pour la définition de fonctions anonymes car il ressemble (un peu) au caractère λ.

Style "sans point" (pointless)

La fonction (.) permet de composer deux fonctions. Il faut lire le point comme le "rond" en mathématiques. Ainsi f(g x) est équivalent à (f . g) x.
De plus, une fonction s'écrivant h x = (f . g) x peut aussi s'écrire "sans point" h = f . g
Une source de confusion (et aussi de blagues) vient du terme pointless. Pourquoi appelle-t-on ce style "sans point", alors que justement on utilise le point ? En réalité, dans cette expression, le point ne fait pas référence au caractère, mais à l'argument d'une fonction (en anglais mathématique, on appelle cela le point). Et effectivement, dans une expression pointless, on n'écrit pas les arguments d'une fonction lorsqu'on la définit.

Compréhension de liste (list comprehension)

Les compréhensions de liste sont déjà connues des développeurs Python ou Erlang. Les programmeurs Scala pourront aussi trouver des similarités avec le for de ce langage.

La compréhension de liste permet de transformer et de filtrer des listes dans une même expression. On peut le voir comme une combinaison de filter et de map.

La syntaxe est très inspirée de la notation de définition des ensembles en mathématiques. Par exemple, nous définissons ici la "liste des élément x multipliés par 10, pour tout x dans la liste des entiers de 1 à 10 tels que x est impair":

Les compréhensions de liste sont un élément syntaxique très intéressant pour l'expressivité et la clarté du code. Nous n'avons fait ici qu'effleurer les possibilités qu'elles offrent.

Pour aller plus loin

Si vous avez apprécié cet article, sachez que d'autres sont en préparation. Nous aborderons la déclaration des type de données afin de modéliser une version simplifiée de réseau social, ainsi que d'autre manière de manipuler les listes.