Innov and Co

Blog

Le compilateur GHC Haskell en version 8.0.1

Le compilateur GHC Haskell en version 8.0.1

Lorsque le compilateur GHC se distingue de la spécification du langage, il le fait par le biais d'extensions optionnelles. C'est une approche qui permet de garder une rétro compatibilité forte. En contrepartie, écrire du code Haskell sans certaines extensions est assez déprimant et on est arrivé à une situation où GHC est le seul compilateur capable de faire tourner la plupart des codes disponibles.

DeriveAnyClass
L'extension DeriveAnyClass s'est vue améliorée et est moins restrictive. En Haskell, du comportement générique sur des types est ajouté par le biais de "typeclass". C'est proche des "concepts" ou "traits" d'autres langage. Dans de nombreux cas ces comportements sont implémentés de façon générique et peuvent ainsi être dérivés automatiquement. Pour l'exemple nous allons implémenter un arbre binaire.

Pour commencer un peu de boilerplate pour importer les modules et extensions nécessaires :

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveAnyClass #-}

import GHC.Generics
import Data.Serialize
Maintenant nous allons définir notre type d'arbre binaire :

data Tree t = Node t (Tree t) (Tree t) | Leaf
deriving (Show, Serialize, Functor, Foldable, Generic)
Ici pas mal de choses à expliquer. data Tree t signifie que nous créons un type Tree polymorphe paramétré par un type arbitraire t. C'est l'équivalent du C++ template class Tree {}. Cette arbre peut avoir deux représentations, équivalentes d'une union C, mais typée :

soit un Node associé à trois valeurs : une valeur arbitraire de type t, et deux sous arbres Tree t.
soit une feuille Leaf.
Ainsi un arbre binaire vide sera représenté par Leaf. Alors que l'arbre binaire suivant :

5
/ \
3 6
/ \ / \
0 .. .
Sera représenté par :
Node 5
(Node 3
(Node 0 Leaf Leaf)
Leaf
)
(Node 6 Leaf Leaf)
Pour finir, la clause deriving (Show, Serialize, Functor, Foldable, Generic) liste les comportements par défaut que nous voulons automatiquement dériver :

Show permet d'obtenir la méthode show qui permet d'afficher l'arbre.
Serialize nous permet de serializer / déserializer notre arbre.
Functor nous permet d'appliquer une opération sur tous les éléments de l'arbre avec fmap.
Foldable nous permet d'effectuer des opérations de réduction sur l'arbre. L'implémentation par défaut ne profite pas de la structure d'arbre binaire pour optimiser la recherche du maximum et du minimum, il faudrait surcharger spécifiquement ces fonctions.
Soit :

-- Show
> let tree = Node 5 (Node 3 (Node 0 Leaf Leaf) Leaf) (Node 6 Leaf Leaf)
> print tree
Node 5 (Node 3 (Node 0 Leaf Leaf) Leaf) (Node 6 Leaf Leaf)

-- Serialize
> encode tree
"\NUL\NUL\NUL\NUL\NUL\ENQ\NUL\NUL\NUL\NUL\NUL\ETX\NUL\NUL\NUL\NUL\NUL\NUL\SOH\SOH\SOH\NUL\NUL\NUL\NUL\NUL\ACK\SOH\SOH"

-- Functor
> fmap (*2) tree
Node 10 (Node 6 (Node 0 Leaf Leaf) Leaf) (Node 12 Leaf Leaf)

-- Foldable
> maximum tree
6
> minimum tree
0
> sum tree
14
Les nouveautés de GHC 8.0 vont permettre de rendre encore plus générique et facile ce type de comportement par défaut.

PatternSynonyms
Haskell permet le pattern matching basé sur les types, c'est une forme de switch/case avancé. Par exemple on peut imaginer un type Date :

data Date = Date Int Int Int
Et une fonction qui a une liste de dates spéciales associe une chaine :

evenements date = case date of
Date 1 1 year -> "Premier jour de l'année " ++ (show year)
Date 24 12 _ -> "Noél"
Date _ 7 _ -> "Le mois de Juillet"
Date 6 6 _ -> "Joyeux anniversaire"
_ -> "Jour ininteressant"
Ici le pattern Date permet de réaliser des comparaisons absolues ou partielles (_ est le joker) et permet aussi d'affecter des variables comme ici year. Le souci c'est que dans certains cas, les patterns par défaut ne sont pas assez expressifs et deviennent trop complexe. Il est possible de mettre en place des synonymes :

{-# LANGUAGE PatternSynonyms #-}

pattern Year x <- Date _ _ x
pattern Noel <- Date 24 12 _
pattern Month x <- Date _ x _
pattern Birthday x y <- Date x y _
pattern FirstOfYear x = Date 1 1 year
Permettant de réécrire le code précédent :

evenements date = case date of
FirstOfYear year -> "Premier jour de l'année " ++ (show year)
Noal -> "Noel"
Month 7 -> "Le mois de Juillet"
Birthday 6 6 -> "Joyeux anniversaire"
_ -> "Jour inintéressant"
Ceci permettant de rendre le code bien plus lisible localement. GHC 8.0 apporte donc son lot de nouveautés sur le support des patterns synonymes avancés.

En vrac
Un meilleur support pour les informations de debug DWARF. Ce qui permet d'utiliser en Haskell tous les outils autour de DWARF, comme le debug de code avec "gdb", le profiling avec "perf" ou la couverture de code.
Les extensions Strict et StrictData. Par défaut Haskell est un langage paresseux ce qui veut dire que les calculs ne sont effectués que lorsque la valeur est nécessaire et non pas au moment de l'appel de fonction. Cela apporte pas mal de souplesse, cela rend certains algorithmes facile à écrire, mais ce n'est pas toujours optimal d'un point de vu performance. Haskell permet de désactiver l'évaluation paresseuse localement, mais c'est souvent source d'erreur et de complexité du code. Ces nouvelles extensions permettent de désactiver l'évaluation paresseuse totalement au sein d'un module. C'est l'un des reproches les plus courants faits à Haskell qui disparaît.
L'extension DuplicateRecordFields est sans doute un des points les plus importants de cette version. Imaginez un type Point2D avec les champs x et y et un type Point3D avec les champs x, y et z et imaginez un instant que cela ne soit pas possible de les faire cohabiter dans le même module… Impensable non ? Et bien c'était le cas en Haskell avant GHC 8.0 et c'est une vraie libération qui va permettre de simplifier beaucoup de code.
Un meilleur support des piles d'appel, notamment lors de l'affichage d'une exception. Il est maintenant possible de savoir quelle fonction est responsable de cette exception.
Le développeur peut maintenant paramétrer les erreurs de type, ce qui permet de rendre les bibliothèques plus utilisables car à la place d'une erreur de type simple disant que votre type n'est pas compatible, le développeur de la bibliothèque peut vous expliquer pourquoi.
Un meilleur support de nombreuses architectures et notamment ARM.
Extension TypeInType qui permet à n'importe quel type d'être élevé au rang de kind, c'est à dire le type d'un type. C'est un premier pas vers un meilleur support du typage dépendant. En très simplifié cela revient à ajouter des contraintes sur les valeurs d'un types qui seront vérifiées à la compilation. Un exemple simple serait celui d'un tableau de taille fixe. On pourrait définir la concaténation de deux tableaux contenant respectivement N et M éléments comme un tableau de taille fixe contenant N + M éléments. Un autre exemple serait un tuple contenant un booléen et un entier, en Haskell son type est (Bool, Int). Par conception, on voudrait que l'entier soit positif si le booléen est à True et négatif sinon. Le système de type pourrait permettre de représenter cela. Un autre exemple, si vous avez une fonction qui ne s'applique que sur une liste non vide, il pourrait être possible de rajouter cette contrainte dans le type d'entrée de la fonction.
GHC peut se servir de LLVM comme générateur de code. Il a été décidé de forcer la version de LLVM utilisée pour une version spécifique de GHC (3.7 pour GHC 8.0) afin de simplifier la maintenance.
Base est la bibliothèque standard associée à GHC et est maintenant disponible en version 4.9.0.0. Changements majeurs :

Certaines fonctions comme error and undefined affichent la pile d'appel lorsque utilisée, cela est plus facile pour corriger les bugs.
Ajout de Data.List.NonEmpty qui représente des listes non vides. Ce type est pratique si lors de votre conception vous savez que vos listes ne peuvent être vides. Cela permet de simplifier et de réduire les risques d'erreur car certaines fonctions, comme maximum, qui ne sont pas définies sur les listes vides, peuvent être appelées sans crainte.
Ajout Data.SemiGroup qui représente l'ensemble des types possédant une opération de réduction mais pas d'élément neutre. J'en parlais dans ce journal sur le tri en Haskell. Les Monoids sont les types qui admettent une opération de réduction et un élément neutre, comme la somme pour les entiers, qui admet 0 comme élément neutre, ou la concaténation de chaînes de caractère, qui admet la chaîne vide comme élément neutre. Les entiers admettent l'opération de réduction minimum, mais il n'existe pas d'élément neutre, ainsi ce ne sont pas des Monoids mais ce sont des SemiGroups.
Ces deux nouvelles bibliothèques sont plutôt représentatives de la philosophie de typage qui existe en Haskell. Là où dans de nombreux langages on utiliserait naïvement un entier pour stocker le calcul d'un minimum, en Haskell nous utiliserons un type Min Int. Celui-ci a l'avantage de ne permettre que des calculs de minimum, là ou l'entier permet de nombreuses choses comme l'addition, la soustraction, le modulo, … En Haskell on essaye de restreindre les types à leur fonction et rien de plus. Cela permet de documenter par le type et en second lieu on évite des erreurs futures en ne rendant pas des opérations fausses disponibles. Par exemple, cela a du sens de faire une addition entre deux monnaies identiques, mais pas de sens de multiplier celles-ci. Une modélisation qui permet la multiplication permet un jour l'usage de celle-ci et ainsi l'introduction d'un bug.

L'écosystème Haskell se met aussi à jour.

Stack propose depuis le 26 mai 2016 une nighly incluant ghc 8.0.1. On rappel que Stack est un outil de gestion de dépendance pour Haskell qui se charge de télécharger les dépendances de votre projet et de construire celui-ci. La liste des dépendances possibles est disponible sur Stackage qui réalise un travail de sélection des paquets fonctionnant entre eux. C'est ainsi une garantie de pouvoir compiler votre projet avec un environnement identique à celui du développement. Stackage est un sous-ensemble de Hackage qui liste tous les packages disponibles pour Haskell. On note qu'il est très facile de commencer avec Haskell et stack puisque une fois stack installé, il suffit d'une commande pour qu'il installe lui-même le compilateur et les dépendances nécessaires.
Hoogle et Hayoo! les moteurs de recherche de fonctions dans Hackage supportent la bibliothèque base 4.9.0.0. Vous pouvez donc chercher des fonctions grâce a leurs signatures.
Digressions
GHC 8.2 est prévu pour Avril 2017 d'après la page de Status.

GHC va bientôt passer sur un nouveau système de build basé sur Shake. Je vous encourage à regarder, c'est un remplaçant intéressant à make / cmake qui est performant et typé statiquement, avec quelques fonctionnalités intéressantes.

Je vous encourage aussi à regarder l'outil Liquid Haskell qui propose de la vérification statique de prédicats sur votre code Haskell et qui est un bon complément au typage pour rendre son code encore plus robuste.
Un autre tutoriel, plus facile à suivre, http://ucsd-progsys.github.io/liquidhaskell-tutorial/01-intro.html

Nous contacter

Nos bureaux sont situés à Paris, Compiègne et Lille.
Notre siège social est situé au 11, avenue du Maréchal Foch 60200 Compiègne

Pour le Service Client : contact@functionalprog.fr