Sommaire
Haskell en trois mots
Haskell est un langage de programmation purement fonctionnel, avec un typage statique fort, et une stratégie d'évaluation non stricte.
Le Glasgow Haskell Compiler est le principal compilateur Haskell, bien qu'il en existe d'autres.
Quelques exemples
L'inévitable hello world :
main = putStrLn "Saluton Ĉirkaŭaĵojn!"
Suite de Fibonnacci :
fibs :: Int -> Int
fibs 0 = 0
fibs 1 = 1
fibs n = fibs (n-1) + fibs (n-2)
-- Sous forme de liste infinie
fibs':: [Int]
fibs' = 0:1:(zipWith (+) fibs' (tail fibs'))
Un langage fonctionnel pur
La spécificité la plus marquante de Haskell, c'est qu'il s'agit d'un langage fonctionnel pur : une fonction appelée avec les mêmes arguments renverra toujours le même résultat, contrairement à une fonction comme input(message)
en python.
Cela rend possible la transparence référentielle : une expression peut être remplacée par son résultat sans changer le comportement de l'application. Le raisonnement sur les équations est mené par substitution sans inquiétude des effets de bord.
Les entrée-sorties et autres effets de bord sont bien sûr possibles, mais ils sont distingués dans le système de type des calculs sans effets de bord. Cela permet des optimisations assez poussées et garanties correctes sur ces derniers. Cette distinction des effets de bord dans le système de type est déroutante au début, mais avec l'expérience, elle apporte un réel confort.
Avec un typage puissant
Le typage statique permet de vérifier à la compilation la cohérence d'un programme et de détecter de nombreuses classes d'erreurs fréquentes. Un type statique est une approximation réalisée pendant la compilation des valeurs rencontrées au cours de l'exécution. Il est extrêmement difficile de concevoir un système de type à la fois flexible (faiblement verbeux, avec une inférence utile), complet (de grain fin, éliminant plus de bugs) et correct (pas de programmes justes rejetés). Haskell essaye de trouver le juste milieu pour que la programmation généraliste soit efficace.
Les aficionados du typage dynamique et du « duck typing » argumentent que, lors du développement avec les langages statiquement typés (typiquement C/C++), une grande partie des ressources sont gaspillées à combattre le système de type. Le typage statique de Haskell est rendu extrêmement flexible grâce au polymorphisme paramétrique, similaire aux templates en C++, mais avec des possibilités d'inférence bien plus développées.
Un type polymorphique peut être restreint à l'aide de contraintes, c'est-à-dire un ensemble de spécifications (ou typeclasses). Par exemple pour insérer un élément dans un Set
, il est nécessaire de pouvoir le comparer aux éléments déjà présents avec la méthode compare
de la typeclasse Ord
, d'où la contrainte Ord a =>
dans le type de insert
:
insert :: Ord a => a -> Set a -> Set a
En pratique, dans les cas courants, la seule différence induite par le système de types entre un code haskell et le code python correspondant est la présence (facultative mais habituelle) en haskell de la signature de chaque fonction… Signatures qui sont en cours d'adoption par la communauté python.
Les nouveautés de ghc 7.8.1
Changements du langage
Typed holes
Grâce au typage fort de haskell, on se trouve souvent à utiliser les types pour guider la conception : on écrit d'abord les types dont on va avoir besoin, puis les signatures des fonctions. Ensuite, de proche en proche, l'implémentation des fonctions est guidée par les signatures. Bien sûr, dans les cas les plus complexes, il faut d'autres ressources que cette méthode, mais on y devient rapidement accro. L'extension typed holes (littéralement « trous typés ») permet d'utiliser cette démarche dans le corps d'une fonction.
Cette extension est souvent utilisée conjointement avec le drapeau -fdefer-type-errors qui transforme les erreurs de typage en warnings. Dans ghci, cela permet de charger toutes les définitions et de les manipuler même si leurs types sont incorrects. Une expression mal typée ou contenant des typed holes est équivalant à undefined et déclenchera une erreur uniquement si elle est évaluée.
En pratique, un hole est un _
placé dans le code à la place d'une expression manquante (ici à la dernière ligne):
getSensorReading :: IO Double
getSensorReading = return 123.45
-- TODO: utiliser vraiment la sonde thermique
main = do
tKelvins <- getSensorReading
putStrLn ("Aujourd'hui il fait " ++ _ ( tKelvins - 273.15) ++ " degrés")
À la compilation, ghc émet une erreur (ou un warning si -fdefer-type-errors est activé) pour chaque hole, et affiche son type:
Found hole ‘_’ with type: Double -> [Char]
Relevant bindings include
tKelvins :: Double (bound at toto.hs:6:5)
main :: IO () (bound at toto.hs:5:1)
In the expression: _
In the first argument of ‘(++)’, namely ‘_ (tKelvins - 273.15)’
In the second argument of ‘(++)’, namely
‘_ (tKelvins - 273.15) ++ " degrés"’
Dans notre exemple, le warning à la compilation nous indique que pour afficher la température dans le putStrLn
, il nous faudra une fonction de type Double -> String
. Une simple recherche dans hoogle nous indique que la fonction en question n'est autre que show
.
OverloadedLists
Haskell repose sur une syntaxe très concise, la majeure partie du langage (Opérateurs, type de base, etc) étant définie dans le Prelude.
En revanche, ce n'est pas le cas des expressions littérales comme les nombres, les chaines de caractères et les listes. Par exemple [1, 2, 3]
est interprété par le compilateur comme 1:2:3:[]
, où (:)
et []
sont les constructeurs du type liste défini dans le Prelude. OverloadedStrings autorisait déjà les chaînes de caractères littérales avec d'autres types que [Char]
, notamment le type Text
pour l'encodage UTF-8.
Cette extension OverloadedLists est documentée, elle permet d'utiliser la syntaxe des littéraux listes avec d'autres types, comme par exemple les vecteurs, les chaînes du module Text
ou encore les ensembles.
Pour utiliser cette extension, il faut disposer d'une instance de la classe IsList
. Il faudra donc attendre que ces instances soient intégrées aux bibliothèques haskell pour vraiment en profiter.
PatternSynonyms
Une autre extension, PatternSynonyms permet de donner un nom à un motif pour le filtrage.
La puissance des types de donnée algébrique (ADT) repose sur la possibilité de filtrer les données par motif, comme en Scala :
type Errno = Int
data Error = ErrorLibC Errno
| ErrorIO IOException
errorToStr :: Error -> String
errorToStr (ErrorIO ioe) = "Haskell's IOException: " ++ show ioe
errorToStr (ErrorLibC errno) = "LibC's errno: " ++ show errno
Chaque constructeur du type Error
défini un motif, utilisé par la fonction errorToStr
pour filtrer les erreurs. En revanche il n'était pas possible de définir ses propres motifs en plus des constructeurs. PatternSynonyms permet maintenant cela, par exemple pour avoir plusieurs motifs par constructeurs :
pattern ErrorPERM = ErrorLibC 1
pattern ErrorNOENT = ErrorLibC 2
pattern ErrorSRCH = ErrorLibC 3
errorToStr :: Error -> String
errorToStr (ErrorIO ioe) = "Haskell's IOException: " ++ show ioe
errorToStr ErrorPERM = "Operation not permitted"
errorToStr ErrorNOENT = "No such file or directory"
errorToStr ErrorSRCH = "No such process"
errorToStr (ErrorLibC errno) = "LibC's unknown errno: " ++ show errno
Les motifs synonymes sont aussi utilisés pour cibler des données profondément imbriquées : pattern Greetings name = 'h':'e':'l':'l':'o':' ':name
. Par défaut il sont bidirectionnels : Greetings "world"
est une valeur. Il est possible de définir des synonymes unidirectionnels lorsqu'il n'existe pas de bijection.
TypeFamilies fermées
Il est possible de déclarer une famille de type et ses instances dans la même clause, par exemple:
type family TF a where
TF ([Bool]) = Int
TF ([Bool],[Bool]) = Double
La famille est alors fermée, c'est-à-dire qu'il n'est pas possible de déclarer de nouvelles instances de TF
.
Améliorations du compilateur
De nombreuses améliorations portent sur le compilateur lui-même, sans modifier le langage. Il est toujours plus rapide et économe en mémoire, notamment pour l'inférence de types. La compilation en parallèle de modules est activable avec la nouvelle option -jN
.
Sur Linux, FreeBSD et Mac OS X, GHCi
utilise maintenant le chargeur de code dynamique du système. Auparavant, un chargeur maison liait dynamiquement du code statique (.a
) en mémoire. Pour utiliser cette fonctionnalité, il est nécessaire de compiler avec -dynamic-too
pour générer des bibliothèques dynamiques (.dyn.so
).
Améliorations de la génération de code
La qualité du code que ghc
produit s'améliore aussi :
-
ghc
est capable d'utiliser plus de types «nus» (unboxed), avec l'activation de -funbox-small-strict-fields
par défaut;
- le gestionnaire d'entrée sortie se parallélise mieux, «de façon linéaire jusqu'à 32 cœurs»;
- le nouveau générateur de représentation intermédiaire Cmm a été activé, après une gestation de plusieurs années;
- de nouvelles primitives (PrimOps) pour les instructions SIMD sont disponibles, pour l'instant uniquement avec le générateur LLVM.
Amélioration du gestionnaire d'entrée-sorties
Le gestionnaire d'entrée-sorties était un des points les plus critiques de la performance du runtime haskell. Dans la version 7.8.1, il a été grandement amélioré, en particulier dans un contexte multi-processeurs. Haskell devient ainsi de plus en plus crédible comme meilleur langage impératif du monde.
Compilation vers iOS
La gestion de la compilation croisée a été améliorée, ce qui permet notamment de compiler vers les systèmes iOS.
Compilation vers javascript avec ghcjs
La sortie de la version 7.8 devrait être accompagnée de la première version publique de ghcjs
, un compilateur haskell vers javascript. Ce langage devient de plus en plus l'assembleur du client web et ghc prend ainsi en charge cette plateforme. Par rapport à Fay, qui est plus ancien, ghcjs cherche à compiler tout le langage haskell et pas seulement un sous-ensemble.
L'introduction à ghcjs permet de se faire une bonne idée de la façon de l'utiliser, en particulier pour les parties spécifiques à javascript.
Vers ghc 7.10
La version 7.10.1 sera la prochaine version majeure de ghc. On trouve sur le trac de ghc une liste de choses qui devraient figurer dans cette prochaine version. Certaines nouveautés de cette version sont préparées dès la 7.8.
Monad / Applicative
Ces deux concepts permettent d'associer un effet à une valeur : IO Int
peut par exemple représenter l'action de la lecture d'un entier présent dans un fichier. La notion de foncteur applicatif est plus générale : toutes les monades sont des foncteurs applicatifs, tandis que l'inverse n'est pas nécessairement vrai. L'exécution des effets d'une monade peut dépendre des valeurs obtenues des effets précédents, contrairement aux foncteurs applicatifs où les effets sont exécutés dans l'ordre des arguments.
La classe de type Monad fut introduite dans les premières versions du langage pour gérer les entrées/sorties avec la monade IO
. Lorsque, plus tard, la classe de type Applicative apparût dans la bibliothèque de base, Monad n'a pas été modifiée pour avoir Applicative comme super-classe.
Le but de la proposition Functor < Applicative < Monad est de corriger ce problème pour réduire la redondance (par exemple entre return
et pure
, liftM
et fmap
).
Ce changement, susceptible de rendre du code incompatible, est déjà signalé par un warning dans GHC 7.8 et sera officialisé dans la version 7.10.
Autres actualités de haskell
Olivier Charles a publié sur son blog l'édition 2013 de sa chronique 24 Days of Hackage qui offre une série d'introductions (avec des exemples) aux paquets majeurs disponibles sur Hackage. L'édition 2012 vaut également le détour, car elle couvre des paquets encore plus incontournables (base, containers, transformers, etc.).
Hackage 2
Le site hackage qui répertorie les paquets haskell libres, exécutables et bibliothèques est passé en version 2 en septembre 2013. L'aspect visuel a été amélioré, la recherche concerne maintenant aussi la description des paquets. La plupart des informations sont plus accessibles, en consultation mais aussi à la modification : le nouveau hackage est plus permissif, et il a une API rest plus étendue.
D'autres fonctionnalités ont été développées mais pas encore déployées, comme la gestion flexible des tags et le calcul des dépendances inverses.
Haskell platform
Il n'est généralement pas recommandé d'installer ghc seul, mais plutôt de récupérer la plateforme haskell, qui contient ghc ainsi que les bibliothèques les plus essentielles du monde haskell. Comme la version 7.8 de ghc a tardé, la plateforme n'a pas été mise à jour depuis la version 2013.2 de mai 2013. Une nouvelle version, adaptée à ghc 7.8 devrait sortir prochainement.