URL: https://linuxfr.org/news/ghc-9-2 Title: GHC 9.2 Authors: Guillaum palm123, Ysabeau, tisaac et theojouedubanjo Date: 2021-08-17T11:03:48+02:00 License: CC By-SA Tags: Score: 3 GHC 9.2 est sorti le 29 octobre 2021. Cette nouvelle version du principal compilateur pour Haskell apporte son lot de nouveautés détaillées dans la suite de cette dépêche. Comme à notre habitude, nous terminerons la dépêche par un exemple de projet en Haskell. Nous rappelons qu’Haskell est un langage de programmation qui se démarque par son design. En effet, fort d’un typage statique avec inférence (i.e. il n’est pas nécessaire d’écrire les types pour que le langage les vérifie), son évaluation paresseuse (le code n’est exécuté que quand c’est strictement nécessaire) et de sa séparation des effets, Haskell est un langage ovni dans le marché qu’il influence depuis de nombreuses années. ---- [GHC 9.2.1-alpha2 now available](https://www.haskell.org/ghc/blog/20210422-ghc-9.2.1-alpha2-relased.html) [Note de version](https://ghc.gitlab.haskell.org/ghc/doc/users_guide/9.2.1-notes.html) [Annonce de version](https://discourse.haskell.org/t/ghc-9-2-1-released/3527) ---- # Notes de version ## RecordDotSyntax et NoFieldSelectors Les nouvelles extensions `OverloadedRecordDot` `NoFieldSelectors`, `OverloadedRecordUpdate` ainsi que le support de `DuplicateRecordFields` avec l’extension `PatternSynonyms` impactent un point important du langage : la syntaxe des records. Pour comprendre celui-ci, ainsi que son impact potentiel sur le langage, il faut un peu de contexte. ### Records Haskell permet de définir des records, c’est-à-dire des types ayant plusieurs champs nommés. Par exemple : ```haskell data Joueur = Joueur { nom :: String, score :: Int } deriving (Show) ``` La création, mise à jour et lecture des champs d’un `Joueur` se font de la façon suivante, ici dans une session interactive : ```haskell >>> unJoueur = Joueur { nom = "Guillaume", score = 9001 } >>> unJoueur Joueur {nom = "Guillaume", score = 9001} >>> -- Update >>> unAutreJoueur = unJoueur { score = 10000 } >>> unAutreJoueur Joueur {nom = "Guillaume", score = 10000} >>> -- Lecture d’un champ >>> score unAutreJoueur 10000 ``` On rappelle que Haskell est un langage qui privilégie la non mutabilité, c’est-à-dire que l’on ne peut pas modifier `unJoueur`, il faut donc créer une nouvelle valeur `unAutreJoueur` à chaque mise à jour. Cette syntaxe de record possède de nombreuses limitations. -  Les lectures et mises à jour sur des structures profondes sont généralement complexes. Par exemple, en imaginant que notre joueur est stocké dans le champ `joueur` d’une autre variable `jeu`, si on veut mettre à jour le score de notre joueur, il faudra écrire : ```haskell nouveauJeu = jeu { joueur1 = joueur1 jeu { score = score (joueur1 jeu) + 1 } } ``` C’est extrêmement verbeux. - La création d’un type (ici `Joueur`) va créer autant de fonctions qu’il y a de champs. Dans notre exemple précédent, les fonctions `nom` et `score` seront créées. Cela génère quantité de fonctions qui peuvent entrer en conflit avec d’autres fonctions (comme la fonction `id`). - Le langage Haskell ne permet pas, par défaut, que deux types différents aient les mêmes noms de champs. L’extension `DuplicateRecordFields` supprime cette limitation, cependant les mises à jour et accès aux champs peuvent rester ambigus dans certaines situations. - Il n’est pas possible de réaliser des fonctions polymorphiques sur les noms de champs. Ainsi, imaginons la fonction suivante: ```haskell afficherNom obj = nom obj ``` On pourrait imaginer que, de façon similaire à Python, ou C++, cette fonction puisse accepter n’importe quel objet à condition qu’il ait un attribut `nom`. Hé bien non, cette fonction est ambiguë et le développeur devra choisir et implémenter autant de fonctions qu’il veut gérer de types différents, même si ces fonctions sont toutes les mêmes. ### Un début de solution, `lens` Les « lens » sont un ensemble de fonctionnalités qui permettent la manipulation de « chemins » dans des structures de données, puis l’utilisation de ces chemins pour lire (`view`), modifier (`over`) ou écraser (`set`) une donnée. Le problème est qu’il faut manuellement définir des « lens » pour chacun des champs auxquels on souhaite accéder. La librairie `lens` propose de réaliser cela par le biais de `TemplateHaskell`. ```haskell data Joueur = Joueur { _score :: Int, _nom :: String } deriving (Show) makeLenses ''Joueur data Jeu = Jeu { _joueur1 :: Joueur, _joueur2 :: Joueur } deriving (Show) makeLenses ''Jeu ``` `makeLenses` va générer les lens `score`, `nom`, `joueur1` et `joueur2`. Maintenant on peut faire des choses : ```haskell >>> unJeu = Jeu { _joueur1 = Joueur { _score = 0, _nom = "Guillaume"}, _joueur2 = Joueur { _score = 0, _nom = "Valérian" }} >>> unJeu Jeu {_joueur1 = Joueur {_score = 0, _nom = "Guillaume"}, _joueur2 = Joueur {_score = 0, _nom = "Val33rian"}} >>> view (joueur1 . score) unJeu 0 >>> unJeu' = set (joueur1 . score) 100 unJeu >>> view (joueur1 . score) unJeu' 100 >>> unJeu'' = over (joueur1 . score) (*2) unJeu' >>> unJeu' Jeu {_joueur1 = Joueur {_score = 100, _nom = "Guillaume"}, _joueur2 = Joueur {_score = 0, _nom = "Val33rian"}} ``` Les « lens » règlent le problème de la modification en profondeur d’une structure de donnée, cependant les problèmes de conflit de nom et de mises à jour polymorphiques restent. De plus s’ajoute un nouveau problème. Il existe de nombreuses librairies de lens, avec des approches différentes. On peut citer `lens` et `optics`. Ces librairies sont impressionnantes de fonctionnalités (bien que l’on puisse se limiter au sous-ensemble que je viens de présenter), les erreurs du compilateur peuvent être dures à lire. ### « Generic-lens » Le paquet [generic-lens](https://hackage.haskell.org/package/generic-lens) (et ses variantes pour d’autres type de lens, comme [generic-optics](https://hackage.haskell.org/package/generic-optics)) permettent de générer des lens avec une syntaxe différente. Là où le package précédent générait une lens dans l’espace de nom des fonctions (e.g. `score` dans l’exemple d’avant), ces nouveaux packages permettent de créer des lens en utilisant des chaînes de caractère au niveau du type `field @"joueur1` ou des « labels », `#joueur1`. La lens `joueur1 . score` devient alors `field @"joueur1" . field @"score"` ou `#joueur1 . #score`. L’avantage de cette approche est qu’il n’y a plus de conflit d’espace de nom et que les lens peuvent être polymorphiques, c’est-à-dire s’appliquer sur le même champ de type différent. Les inconvénients de ces approches sont les suivants : - la syntaxe est soit « verbeuse » (`field @"joueur1`), ou utilise les labels (e.g. `#joueur1`), nécessitant `OverloadedLabels`, qui est une syntaxe assez récente dans GHC. Celle-ci est peu utilisée, mal connue, la syntaxe est nouvelle et elle pose son lot de problème. Par exemple, avec la libraire `lens`, les labels génèrent des instances orphelines. Ce n’est pas le cas avec la bibliothèque `optics`. - Cela demande le choix de l’utilisation d’une bibliothèque de lens, ce qui limite l’adoption. - GHC va toujours créer les fonctions pour nos sélecteurs. Ainsi, deux types ayant les mêmes noms de champs vont générer les mêmes sélecteurs et ainsi générer des conflits, même si ceux-ci ne sont pas utilisés. ### `RecordDotSyntax` et `NoFieldSelectors` L’extension `NoFieldSelectors` permet tout simplement de ne plus exposer les sélecteurs associés aux champs d’un type. Cela supprime tout simplement les problèmes de conflits discutés avant. Les extensions `OverloadedRecordDot` et `OverloadedRecordUpdate` permettent tout simplement d’utiliser une syntaxe assez classique dans d’autres langages de programmation, le `.`, pour accéder aux champs. Ainsi, accéder au champ `nom` du `joueur1` du `jeu` se fait grâce à `jeu.joueur1.nom`. Et la mise à jour en profondeur est aussi possible, par exemple: ```haskell nouveauJeu = jeu { joueur1.score = jeu.joueur1.score + 1 } ``` `OverloadedRecordUpdate` ne permet pas (encore) de mise à jour pouvant changer le type d’un sous champs (ce qui est possible avec les « lens »), et la syntaxe reste plus lourd que les lens dans le cas de mise à jour profonde, comparez l’exemple précédent avec : ```haskell nouveauJeu = over (#joueur1 . #score) (+1) jeu ``` De plus, `OverloadedRecordUpdate` nécessite que l’utilisateur fournisse une fonction `setField` et `getField`, ainsi cela ne fonctionne pas encore directement. Gageons que de futures versions de GHC fourniront des fonctions adaptées par défaut. ### Conclusion Les lens en général restent plus puissantes que cette extension, et la librarie `optics`, avec les labels et l’absence de conflit de nom grâce à `NoFieldSelectors` apportent à mon gout plus de souplesse. Cependant l’arrivée de ces changements au niveau des records en Haskell apportent une solution « officielle » aux problèmes des record et devrait simplifier l’adoption d’Haskell par les débutants, c’est donc à mon avis une très bonne nouvelle. ## GHC 2021 Vous le savez sans doute, GHC introduit des nouveautés vis-à-vis du standard Haskell par le biais d’extension. Malheureusement ce mécanisme devient ingérable tant la [liste d’extensions est longue]( https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/table.html). Chaque fichier Haskell commence généralement par une liste de multiples extensions, les développeurs hésitent à activer certaines d’entre elles. Pour exemple, l’utilisation de syntaxes `0b01` et `0xfe` pour représenter des nombres respectivement en notation binaire ou hexadécimale, nécessitent l’activation de deux extensions. La nouvelle extension, `GHC2021` regroupe tout un ensemble d’extensions, [46 au total]( https://ghc.gitlab.haskell.org/ghc/doc/users_guide/exts/control.html#extension-GHC2021) et devrait réduire le préambule des fichiers dans un projet. Le processus qui a permis de sélectionner [ces extensions](https://github.com/ghc-proposals/ghc-proposals/blob/master/proposals/0380-ghc2021.rst) est particulièrement intéressant. Chaque extension du langage a été notée en fonction de différents critères tels que son utilisation par la communauté, le risque de « surprise », l’apport au langage… ## En vrac ### Types liftés - l’extension `UnliftedDataTypes` permet de définir des types qui n’acceptent pas d’évaluation paresseuse. Il était déjà possible de forcer l’évaluation par le biais de `BangPatterns` ou de `Strict` et `StrictData`, mais ces extensions n’avaient pas d’impact sur la représentation des données. La nouvelle extension `UnliftedDataTypes` permet ainsi de créer des types n’acceptant pas d’évaluation paresseuse, et ainsi, dans certains cas, de réduire leur taille. Cela sera très utile dans certaines structures de données afin de réduire les indirections de pointeurs qui coûtent en performance. - lié au point précédent, la représentation des types « lifted » ou « unlifted » (i.e. acceptant ou non une version paresseuse et étant oui ou non géré par le ramasse-miette) évolue et permet de représenter des fonctions polymorphiques quelle que soit la représentation des objets utilisée. Ces deux points vont dans le sens de générer du code plus efficace avec moins d’indirection (i.e. `UnliftedDataTypes`) sans payer le coût d’une double implémentation grâce aux fonctions polymorphiques sur la représentation. - `ghc-exactprint` est fusionné dans GHC. La représentation du code après parsing conserve les informations de présentation tel que les espaces blancs, les retours à la ligne, etc. Ainsi il est possible de parser du code Haskell, faire des modifications, et réécrire ce code sans changer la présentation. C’est une grosse avancée pour l’outillage puisque, par exemple, cela améliore l’intégration avec les outils de refactoring d’un IDE qui peuvent maintenant transformer le code (par exemple renommer une variable) sans changer la présentation du code. - Il est maintenant possible de générer de la documentation par le biais de `TemplateHaskell`. En effet, `TemplateHaskell` permet la génération de code pendant la compilation, mais jusqu’à alors, ce code ne pouvait pas être associé à une documentation, c’est maintenant corrigé grâce aux fonctions `putDoc` et `getDoc` qui permettent respectivement de générer une documentation ou de lire une documentation. - l’extension `ImpredicativeTypes` a été complètement revue et est maintenant considérée comme robuste. C’est un détail assez complexe du langage invisible pour beaucoup, mais sachez que cela permet l’instanciation de fonctions plus polymorphiques et que cela impacte un opérateur utilisé tous les jours par les développeurs Haskell, `$`, qui n’est autre que l’application de fonction (e.g. `f x` et `f $ x` sont identiques). En bref, un cas particulier du langage est maintenant géré de manière robuste et sans cas particulier. Je vous renvoie vers l’article qui traite de cela, https://www.microsoft.com/en-us/research/publication/a-quick-look-at-impredicativity/. - Un générateur de code natif pour AArch64 est maintenant disponible. Aarch64 était déjà géré par GHC par le biais du backend LLVM, mais le générateur de code natif est plus rapide. - `LinearTypes` peut maintenant inférer la multiplicité dans les expressions `case`. Dit autrement, on peut utiliser des `case` avec les types linéaires, ce qui n’était pas possible auparavant, l’algorithme n’arrivant pas à « compter » correctement l’usage des références. - Un nouveau warning `-Wredundant-bang-patterns` prévient lors de l’usage inutile d’un bang (i.e. `!`) sur une donnée qui est déjà forcée. Ce n’est pas forcément utile, mais cela peut donner une meilleure compréhension du code. - Le type `Natural` peut maintenant être promu au niveau du "kind", remplaçant le kind `Nat` qui existait avant. `Natural` représente un entier positif. `Nat` permettait de représenter un entier positif paramétrant un type. Par exemple, le kind `Matrix (a :: Nat) (b :: Nat)`, permet de représenter par exemple le type `Matrix 4 4`, où `4` est un nombre entier positif, mais connu dans le type et non pas seulement à l’exécution. La convergence entre `Natural` et `Nat` permet d’écrire des types qui seront utilisés autant à l’exécution qu’en tant que kind. - Le type `Char` peut maintenant aussi être promu au niveau du "kind" et de nouvelles "types families" (i.e. fonctions de type) permettent de composer des `Char` ensemble afin de construire des `Symbol` (i.e. des chaines de caractère au niveau du type). Cela ouvre tout un tas de perspectives de programmation au niveau du type. ## Debug Beaucoup de changements de fond qui vont permettre d’améliorer le processus de debug d’un programme arrivent avec GHC 9.2. ### Origine des allocations La méthode de `hi-profiling` permet de tagger les objets lors de leur allocation en précisant l’origine de l’allocation. Ainsi, lors de l’exécution, il est possible de savoir d’où viennent les objets encore présents en mémoire. Jusqu’à présent il était possible de connaitre l’usage de la mémoire par type d’objet ou le nombre d’allocation par origine dans le code. Mais une fonction qui alloue beaucoup n’est pas forcément une fonction qui utilise beaucoup de mémoire, si les objets alloués ont une durée de vie courte. Plus de détails dans l’article https://well-typed.com/blog/2021/01/first-look-at-hi-profiling-mode/ ### ghc-debug http://ghc.gitlab.haskell.org/ghc-debug/ permet de se connecter à un programme Haskell en cours d’exécution et d’interroger l’état de la mémoire. Jusqu’à présent, les analyses de mémoire ne pouvaient se faire que statiquement, à la fin de l’exécution du programme. ## Performances ### GC Parallel Le GC (Garbage Collector) parallèle a subi de nombreux changements. Sur les programmes parallèles tournant sur plus de 4 "capabilities" (e.g. threads), les temps de pause et le temps CPU utilisé par le GC sont réduits. C’est une avancée importante pour le GC parallèle qui demandait avant beaucoup de réglages manuels pour trouver les paramètres optimaux. Les développeurs de GHC vont jusqu’à annoncer que la plupart des réglages manuels utilisés avant sont inutiles et que les valeurs par défaut seront satisfaisantes dans la plupart des cas. Personnellement, j’attends de tester cela en production puisque jusqu’à présent, j’avais bien trop souvent tendance à désactiver totalement le GC parallèle du fait de ses mauvaises performances. ### Autres - Un programme Haskell aura tendance à rendre plus vite la RAM inutilisée au système, plutôt que de la conserver. L’impact est faible (puisque la RAM inutilisée pouvait être mise dans le SWAP), mais cela peut améliorer la "confiance" en un processus Haskell qui, une fois un pic de consommation passé, affichera une consommation réduite. - La taille de la nurserie par défaut passe de 1 MB à 4 MB. Cette valeur faisait du sens plusieurs années en arrière lorsque la taille des caches des CPUs étaient plus petites. On rappelle que la nurserie est l’endroit ou les objets sont alloués (avant d’être potentiellement déplacés), c’est donc un endroit sous haute pression qui vit dans le cache du processeur, l’augmenter à 4MB permet d’allouer plus d’objets avant de devoir faire tourner le GC, laissant une plus grande chance aux objets temporaires d’être détruits et ainsi améliorant les performances. # Autour de GHC Haskell-language-server, https://hackage.haskell.org/package/haskell-language-server, le LSP pour Haskell est sorti en version 1.4. # Exemple Dans cette section, je voulais parler un peu de formatage. # Haskell et le formatage En Haskell, le formatage est une histoire complexe. Au départ, on fait tout à la main: ```haskell >>> prenom = "Guillaume" >>> age = 35 >>> putStrLn (« Bonjour " <> prenom <> ". Tu as " <> show age <> " ans.") Bonjour Guillaume. Tu as 35 ans. ``` On admettra que cela est peu pratique. C’est difficilement lisible. On se trompe facilement en oubliant un espace. Et on ne peut pas faire de conversion facilement, comme préciser le nombre de chiffres significatifs. La libraire `base`, qui vient de base avec GHC, propose `Text.Printf`: ``` >>> >>> printf "Bonjour %s. Tu as %d ans.\n" prenom age Bonjour Guillaume. Tu as 35 ans. ``` C’est pratique, cela rend quelques services et cela permet de formater: ```haskell >>> printf "%.3f\n" pi 3.142 ``` Mais cette bibliothèque souffre de nombreux défauts, et tout particulièrement: - Pas de support des chaines de plusieurs lignes. - Par défaut, cela génère des `String`, dans un monde ou on aimerait plutôt utiliser `Text` - `printf` n’est pas  sûr et ainsi peut planter lors de l’exécution : ```haskell >>> printf "%s" pi *** Exception: printf: bad formatting char 's' ``` Il existe de nombreuses librairies qui proposent des « mini langages » sous forme de fonctions pour faire du formatage. J’apprécie [fmt](https://hackage.haskell.org/package/fmt-0.6.3.0/docs/Fmt.html), mais cela reste très verbeux: ```haskell >>> let (a, b, n) = ("foo", "bar", 25) >>> ("Here are some words: "+|a|+", "+|b|+"\nAlso a number: "+|n|+"") :: String "Here are some words: foo, bar\nAlso a number: 25" ``` Et cela ne corrige pas le problème des lignes multiples. ## Comment fait Python ? Avec Python, c’est simple, il existe les `f` string: ```python >>> f"Bonjour {prenom}. Tu as {age} ans. Et pi = {pi:.3f}." 'Bonjour Guillaume. Tu as 35 ans. Et pi = 3.141.' ``` C’est simple, c’est lisible, cela permet le formatage avancé, cela permet les lignes multiples. Seul défaut, c’est du Python et ce n’est pas sûr. ## PyF [PyF](https://hackage.haskell.org/package/PyF) c’est ma libraire de formatage pour Haskell. J’ai pris les `f` string de Python, j’y ai ajouté le côté vérifié à la compilation, et on obtient PyF : ```haskell >>> [fmt|Bonjour {prenom}. Tu as {age} ans. Et pi = {pi:.3f}.|] « Bonjour Guillaume. Tu as 35 ans. Et pi = 3.141. » ``` PyF supporte la quasi-totalité du mini langage de formatage des f string de Python. La dernière version, qui sort en même temps que GHC 9.2, a considérablement réduit ses dépendances et ne dépendant maintenant plus que de `GHC`. De plus, de nouveaux formateurs sont apparus : - `str`: une chaîne multi lignes sans formatage - `raw`: une chaîne multi lignes sans échappement - `strTrim` et `fmtTrim`, respectivement une chaîne multi lignes sans et avec formatage, mais avec suppression des espaces blancs dans les deux cas. Les versions `trim` sont tout particulièrement utiles pour respecter l’indentation dans un code : ```haskell main = do putStrLn [fmtTrim| Bonjour {nom}, Voici ma liste de course : - Poivrons {nombreDePoivrons} - Lait {volumeDeLait:.1f} |] ``` Ici, les espaces blancs surnuméraires seront supprimés. # Conclusion GHC 9.2 est là, happy Haskelling. Vous pouvez utiliser `nix`, ou `ghcup`, ou prochainement votre distribution. Ou peut-être un container docker, ou `stack`, bref, essayez GHC 9.2. J'ai tout particulièrement envie de tester : - les meilleures performances du ramasse-miette parallèle ; - `NoFieldSelector` pour ne plus avoir de conflit de nom ; - `GHC2021` pour ne plus commencer tous mes programmes Haskell par 40 lignes d’extensions ; - `ghc-debug` pour debuger. Mais je dirais que je suis un utilisateur avancé. J’ai vraiment hâte de voir comment les changements sur les records vont aider les débutants à s’approprier Haskell.