URL: https://linuxfr.org/news/ghc-8-4-et-8-6 Title: GHC 8.4 et 8.6 Authors: Guillaum gipoisson, Snark, bubarđŸŠ„, palm123, Davy Defaud, Anthony Jaguenaud, karchnu, Bruno Michel, BenoĂźt Sibaud et octachron Date: 2018ćčŽ07月01æ—„T14:38:05+02:00 License: CC By-SA Tags: dĂ©veloppement, haskell et ghc Score: 32 GHC, le compilateur Haskell est sorti en version 8.6.1 le 22 septembre 2018. Cette dĂ©pĂȘche dĂ©taille les nouveautĂ©s. De plus, nous n’avions pas fait de dĂ©pĂȘche pour la version 8.4.1 du 8 mars 2018, ainsi que pour les versions 8.4.2 et 8.4.3 ayant suivi en avril et mai ; cette dĂ©pĂȘche tente de combler ce vide. Les versions [8.4.2](https://mail.haskell.org/pipermail/haskell-cafe/2018-April/128950.html) et [8.4.3](https://mail.haskell.org/pipermail/haskell-cafe/2018-May/129193.html) Ă©tant principalement des versions mineures consacrĂ©es aux corrections de bogues critiques, celles‐ci ne seront pas traitĂ©es dans cette dĂ©pĂȘche. Comme d’habitude pour les dĂ©pĂȘches concernant les sorties de GHC, nous reviendrons sur les nouveautĂ©s de ces versions pour conclure par un petit exemple de Haskell pour vous donner envie d’utiliser ce langage. ---- [PrĂ©cĂ©dente dĂ©pĂȘche pour la sortie de GHC 8.2](https://linuxfr.org/news/sortie-de-ghc-8-2-1) [Apprendre Haskell vous fera le plus grand bien !](http://lyah.haskell.fr) [Le document normalisant/standardisant Haskell : Haskell Report, Version 2010](https://www.haskell.org/onlinereport/haskell2010) [Annonce de la version 8.4.1](https://mail.haskell.org/pipermail/haskell-cafe/2018-March/128730.html) [Notes et guide utilisateur pour cette v8.4.1](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/8.4.1-notes.html) [Annonce de la version 8.6.1](https://ghc.haskell.org/trac/ghc/blog/ghc-8.6.1-released) [Notes et guide utilisateur pour cette v8.6.1](https://downloads.haskell.org/~ghc/8.6.1/docs/html/users_guide/8.6.1-notes.html) ---- # Haskell Haskell est un langage de programmation fonctionnel pur et paresseux. Le « fonctionnel » signifie qu’on axe l’écriture d’un programme plus sur la transformation des donnĂ©es, que sur des sĂ©quences d’instructions Ă  exĂ©cuter (contrairement Ă  un langage impĂ©ratif). Le « pur » signifie qu’on limite au maximum les effets de bord, par exemple, il n’est pas possible de modifier une valeur. Le « paresseux » signifie que les calculs dĂ©crits ne seront rĂ©alisĂ©s qu’au moment oĂč l’on en aura besoin. Cet aspect permet d’avoir des listes infinies en donnĂ©es, par exemple. # Version 8.4 Cette version a apportĂ© beaucoup de nouveautĂ©s pour les utilisateurs et utilisatrices chevronnĂ©s de GHC. Listons rapidement quelques points : - L'extension `TypeInType` a subi des retouches, notamment en cessant de traiter l'opĂ©rateur `*` de façon diffĂ©rente du reste des opĂ©rateurs. Rappelons qu'avec cette extension et au niveau des types, cet opĂ©rateur n'indique plus le type `Kind` d'un type : un type est un `Type`, d'oĂč le nom de l'extension. Cette notion n'est pas trĂšs rigoureuse mais GHC n'est pas non plus un outil de preuve mĂ©canique ! En rĂ©alitĂ©, `TypeInType` est un ajout incrĂ©mental au fonctionnalitĂ©s de l'extension `PolyKinds` et apparentĂ©es et sera complĂštement absorbĂ©e & dĂ©prĂ©ciĂ©e par celles-ci dĂšs la version 8.6. - Le dĂ©couplage des classes `Semigroup` et `Monoid` a franchi une nouvelle Ă©tape. Pour rappel, le changement le plus visible est que l'opĂ©rateur `` se trouve dĂ©sormais dans le module `Data.Semigroup` alors qu'avant le dĂ©couplage il appartenait Ă  `Data.Monoid`. Conceptuellement, les `Semigroup` sont l'ensemble des types qui admettent une opĂ©ration binaire associative, comme le `+`. Un `Monoid` est un `Semigroup` a qui on ajoute un Ă©lĂ©ment neutre, comme le `0`. - Les constantes litĂ©rales hexadĂ©cimales pour les nombres flottants, grĂące Ă  l'extension `HexFloatLiterals`, permettent de representer un nombre flottant en hexadĂ©cimal, `0x0.1p4 == 1.0`. # Version 8.6 La suite de cette dĂ©pĂȘche est consacrĂ©e aux changements de GHC 8.6. Cette liste n'est pas exhaustive et reprĂ©sente la vision des auteurs sur les points importants, amusants, ou pouvant ĂȘtre illustrĂ©s simplement. Les nouveautĂ©s sont intĂ©grĂ©es au sein de GHC par le biais d'extensions, que l'utilisateur aura la libertĂ© d'activer au non. Certaines extensions deviennent officielles lors de la rĂ©daction du [Haskell Report](https://www.haskell.org/onlinereport/haskell2010/), le dernier datant de 2010 et le prochain Ă©tant prĂ©vu pour 2020. Entre temps, les dĂ©veloppeurs Haskell doivent activer de nombreuses extensions. Sous une apparente contrainte, cela apporte au langage une capacitĂ© d'Ă©volution assez intĂ©ressante puisque les dĂ©veloppeurs ne se refusent pas Ă  implĂ©menter des changements importants sous la forme d'extensions. ## Extension Ă  la syntaxe ### `NumericUnderscores` L'extension `NumericUnderscores` permet de mettre des `_` dans ses nombres, comme `123_456_789`. Cela aide la lisibilitĂ©. Notons que jusqu'Ă  prĂ©sent on pouvait facilement exprimer des grands nombres "ronds" en utilisant la notation exponentielle, l'extension `NumDecimals` permettant d'obtenir des nombres entiers le cas Ă©chĂ©ant. ```haskell Prelude> 1e3 1000 Prelude> 1.123e3 1123 Prelude> :type 1.123e3 1.123e3 :: Num p => p -- Un nombre, autant un flottant qu'un entier Prelude> :t 1.1234e3 1.1234e3 :: Fractional p => p -- Ici c'est forcement un flottant, car la valeur 1123.4 ne peut pas ĂȘtre reprĂ©sentĂ©e en entier. -- La nouvelle syntaxe Prelude> 1_123 1123 ``` ### `BlockArguments` La nouvelle extension `BlockArguments` permet un groupement implicite diffĂ©rent des expressions et tout particuliĂšrement des blocs `do` et des fonctions anonymes. Par exemple, le bloc suivant va demander le nom de l'utilisateur et le saluer : ```haskell do putStrLn "Quel est votre nom ?" name <- getLine putStrLn ("Bonjour " ++ name) ``` Pour effectuer cette action en boucle, on peut utiliser `forever` qui prend en argument l'action Ă  rĂ©pĂ©ter Ă  l'infini. Jusqu'alors, il fallait mettre des parenthĂšses disgracieuses Ă  mon goĂ»t : ```haskell forever (do putStrLn "Quel est votre nom ?" name <- getLine putStrLn ("Bonjour " ++ name) ) ``` Ou utiliser l'opĂ©rateur `($)` qui est peu apprĂ©ciĂ© car dĂ©routant pour les dĂ©butants : ```haskell forever $ do putStrLn "Quel est votre nom ?" name <- getLine putStrLn ("Bonjour " ++ name) ``` Dans les deux cas, c'est fort peu confortable. La nouvelle syntaxe, avec l'extension `BlockArguments`, est maintenant : ```haskell forever do putStrLn "Quel est votre nom ?" name <- getLine putStrLn ("Bonjour " ++ name) ``` Cela prend tout son sens sur des tests unitaires, comme avec `HSpec` : ```haskell describe "factorials" $ do it "must be equal 1 when argument is 0" $ do fact 0 `shouldBe` 1 it "must always be positive " $ do property $ \(Positive x) -> fact x>= 1 it "is strictly growing" $ do property $ \(Positive x) -> fact x < fact (x + 1) ``` Notez l'abus de `($)`, qui peut maintenant se remplacer par : ```haskell describe "factorials" do it "must be equal 1 when argument is 0" do fact 0 `shouldBe` 1 it "must always be positive" do property \(Positive x) -> fact x>= 1 it "is strictly growing" do property \(Positive x) -> fact x < fact (x + 1) ``` ## Environment de development ### AmĂ©liorations des « typed holes » Les typed holes sont une syntaxe oĂč le dĂ©veloppeur peut laisser des trous dans son programme et demander au compilateur de l'aide pour les combler. ```haskell _ && True ``` Cette expression Ă©value le ET boolĂ©an entre `_` et `True`. `_` est un `typed hole`, ainsi le compilateur va simplement nous renvoyer un message d'erreur incluant le type dĂ©duit de l'expression, ici `Bool` : ``` :28:1: error: ‱ Found hole: _ :: Bool ‱ In the first argument of ‘(&&)’, namely ‘_’ In the expression: _ && True ... ``` Depuis GHC 8.6, le support s'amĂ©liore et le compilateur fait aussi des suggestions Ă  partir des symboles actuellements accessibles : ```haskell Valid hole fits include otherwise :: Bool (imported from ‘Prelude’ (and originally defined in ‘GHC.Base’)) False :: Bool (imported from ‘Prelude’ (and originally defined in ‘GHC.Types’)) True :: Bool (imported from ‘Prelude’ (and originally defined in ‘GHC.Types’)) ``` À terme on pourra imaginer une intĂ©gration plus poussĂ©e dans les Ă©diteurs avec de l'auto-complĂ©tion, ou avec la documentation et la fonction interactive `:doc`. ### Command `:doc` dans le shell La nouvelle commande `:doc` permet d’accĂ©der Ă  la documentation des fonctions dans le shell `ghci`. Le shell est un des outils les plus importants pour un dĂ©veloppeur Haskell, on y passe beaucoup de temps Ă  expĂ©rimenter. Jusqu'Ă  prĂ©sent on pouvait obtenir des informations sur le type : ```haskell>>> :type reverse reverse :: [a] -> [a] ``` On peut aussi maintenant demander la documentation associĂ©e: ```haskell>>> :doc reverse 'reverse' @xs@ returns the elements of @xs@ in reverse order. @xs@ must be finite ``` Le code Haskell Ă©tant documentĂ© par [Haddock](https://www.haskell.org/haddock/), un langage de documentation intĂ©grĂ© Ă  Haskell, on peut maintenant accĂ©der Ă  ces informations dans le shell. Le fait d'avoir un point d'entrĂ©e standardisĂ© nous laisse imaginer un meilleur support d'outils dans le futur, tel qu'une intĂ©gration aux Ă©diteurs de texte. L'argument `-fshow-docs-of-hole-fits` devrait permettre une interaction entre la documentation et les "typed holes" du paragraphe prĂ©cĂ©dent en affichant la documentation en face des suggestions. ## Extension au mĂ©canisme de programmation gĂ©nĂ©rique ### `DerivingVia` Cette nouvelle extension augmente le mĂ©canisme de dĂ©rivation prĂ©sent dans GHC. Pour bien comprendre cette nouvelle fonctionnalitĂ©, il faut faire un petit rappel sur un point clĂ© d'Haskell, les type-`class` et le mĂ©canisme de dĂ©rivation. #### Notion d'`instance` et de `class` En Haskell, une `class` est une interface et une `instance` est l’implĂ©mentation de celle-ci pour un type. Le vocabulaire n'est pas du tout le mĂȘme que celui utilisĂ© plus habituellement dans des langages objets. Par exemple, on peut imaginer la classe `Shape` : ```haskell class Shape t where area :: t -> Float ``` Qui propose la fonction `area` qui, appliquĂ©e Ă  n'importe quel type implĂ©mentant le type-classe `Shape`, va retourner sa surface. Par exemple : ```haskell -- CrĂ©ation de deux types, 'Square' et 'Rectangle' data Square = Square Float data Rectangle = Rectangle Float Float -- ImplĂ©mentation de 'area' pour les deux instance Shape Square where area (Square side) = side * side instance Shape Rectangle where area (Rectangle a b) = a * b ``` Avec ici deux types, `Square` et `Rectangle` et leur implementation `instance` de la classe. On peut donc maintenant Ă©crire des fonctions polymorphiques fonctionnant uniquement sur les instances de `Shape` : ```haskell print_area shape = do putStrLn "La surface de la forme est :" print (area shape) ``` Nous avons vu ici la crĂ©ation d'`instance`s manuelle. #### `deriving` Haskell propose le mĂ©canisme de `deriving` qui permet de dĂ©river automatiquement des `instance`s pour nos types. Ce mĂ©canisme a beaucoup Ă©voluĂ© et on peut : - dĂ©river des comportements proposĂ©s par le compilateur ou par une bibliothĂšque tierce, ici pour le type `Point3D` nous dĂ©rivons les instances `Show` et `Serialize` qui permettent d'afficher (avec `show`) et de sĂ©rialiser avec `encode` / `decode` : ```haskell data Point3D = Point3D Float Float Float deriving (Show, Serialize) ``` - dĂ©river des comportements d'un type interne si le type n'est qu'une encapsulation du prĂ©cĂ©dent. Par exemple : ```haskell data Counter = Counter Int deriving (Num) ``` `Counter` est maintenant un type qui contient un `Int`, qui implĂ©mente toute la classe `Num` de façon similaire aux `Int`, donc il implĂ©mente les opĂ©rations primaires comme l'addition, la soustraction, etc. Mais ce type n'est pas compatible avec un `Int`. C'est trĂšs pratique pour faire des interfaces robustes. #### `DerivingVia` `DerivingVia` est une nouvelle extension qui ajoute une nouvelle maniĂšre de dĂ©river. On peut imaginer plusieurs types ayant la mĂȘme reprĂ©sentation interne et pour lesquels on veuille dĂ©river le mĂȘme comportement. Au lieu de recopier ce comportement plusieurs fois, on peut faire appelle Ă  `derive via`. Par exemple : ```haskell -- `Normalize` regroupe tous les types pouvant ĂȘtre "normalisĂ©s" class Normalize t where normalize :: t -> t -- Un triplet de points reprĂ©sentant une couleur newtype RGBColor = RGBColor (Float, Float, Float) -- Son instance de 'Normalize' instance Normalize RGBColor where normalize (RGBColor (a, b, c)) = RGBColor (a / s, b / s, c / s) where s = sqrt (a * a + b * b + c * c) -- Le mĂȘme travail est Ă  rĂ©pĂ©ter pour ces nouveaux types, similaires. -- On va gagner du temps avec `deriving via` newtype Direction = Direction (Float, Float, Float) deriving Normalize via RGBColor newtype Position = Position (Float, Float, Float) deriving Normalize via RGBColor ``` Ce travail permet ainsi une plus grande rĂ©utilisation. L'idĂ©e Ă©tant que souvent des types reprĂ©sentant des choses incompatibles peuvent avoir des fonctionnalitĂ©s similaires. On pourrait citer l'exemple des monnaies. Un "Dollar" est incompatible avec un "Euro", ainsi l'addition entre les deux n'a pas de sens. Cependant l'addition entre euros est exactement la mĂȘme que l'addition entre dollars, il est donc dommage d’implĂ©menter celle-ci pour chaque monnaie. ## Greffons de compilation GHC propose une infrastructure de greffons (_plugins_) permettant aux utilisateurs d'Ă©tendre le compilateur sans avoir Ă  mettre les mains dans un code complexe et sans avoir Ă  compiler une nouvelle version du compilateur complet. On trouve trois types de greffons : - Les greffons d'optimisation, qui permettent d'Ă©tendre la phase de gĂ©nĂ©ration de code. Par exemple, [Herbie](https://github.com/mikeizbicki/HerbiePlugin) permet de réécrire des calculs afin de les rendre numĂ©riquement plus stables. - Les greffons de validation de type vont Ă©tendre la phase de validation des types. Par exemple, [thoralf](https://github.com/Divesh-Otwani/the-thoralf-plugin) permet de sous-traiter Ă  un solveur SMT des preuves de type. Cela permet d'exprimer assez simplement des preuves plus complexes. ### Les greffons sources Introduits par GHC 8.6, ils permettent Ă  un dĂ©veloppeur d'observer le code source aprĂšs analyse syntaxique afin de transformer celui-ci ou de simplement le passer en parallĂšle Ă  une tĂąche annexe. Ils ont comme intĂ©rĂȘt, Ă  mon sens, de permettre de rĂ©aliser en parallĂšle de la compilation d'autres tĂąches ayant besoin de l'analyse syntaxique et de rĂ©utiliser celle-ci. Lorsque je compile mon code, je gĂ©nĂšre aussi la documentation, je passe un linter dessus, je passe un formateur dessus et je gĂ©nĂšre des informations comme une base de donnĂ©es des fonctions dĂ©finies, un graphique des modules, etc. Chacun de ces outils demande actuellement une nouvelle phase d'analyse syntaxique rĂ©alisĂ©e par un outil externe qui implĂ©mente souvent sa propre version limitĂ©e et dĂ©fectueuse du parseur. Pouvoir rĂ©aliser ces tĂąches annexes pendant la compilation principale serait donc un gain en temps et en qualitĂ©. GHC 8.6 n'etait pas encore sorti que de nombreux projets s’essayaient Ă  la tache : - [Une réécriture de `graphmod`](http://mpickering.github.io//posts/2018-08-09-source-plugin-graphmod.html) permet de gĂ©nĂ©rer des graphiques reprĂ©sentant les dĂ©pendances entre modules Haskell. - [hlint-source-plugin](https://github.com/ocharles/hlint-source-plugin) permet de rĂ©aliser un [lint](https://en.wikipedia.org/wiki/Lint_(software)), c'est Ă  dire une analyse du style du code. - [idiom-bracket](http://oleg.fi/gists/posts/2018-07-06-idiom-brackets-via-source-pluging.html) L'auteur a judicieusement remarquĂ© qu'il est rare de mettre entre parentheses des listes : on va rarement Ă©crire `([f,g,h])` car les parenthĂšses semblent redondantes. L'auteur "vole" donc cette syntaxe pour exprimer une toute nouvelle construction. Quantified Constraints ---------------------- Ceci est une nouveautĂ© phare de cette version qui simplifie les contraintes lors de la dĂ©finition d'une instance. Prenons un exemple simple pour la mise en contexte. Si on veut dĂ©finir une instance de `Show` pour une liste de `a`, on Ă©crira : ```haskell instance Show a => Show [a] where show = ... ``` Ce qui veut littĂ©ralement dire que `[a]` admet une instance de `Show` si et seulement si `a` admet une instance de `Show`. Ou, dit autrement, on peut afficher une liste de `a` si on sait afficher `a`. Cela a du sens, on va dĂ©finir l'affichage pour la liste, en dĂ©lĂ©guant l'affichage de chaque Ă©lĂ©ment aux fonctions dĂ©jĂ  dĂ©finies pour ces Ă©lĂ©ments. Prenons l'exemple plus Ă©laborĂ© d'un type `User` paramĂ©trĂ© : ```haskell data User f = User { nom :: f String , age :: f Int , superUtilisateur :: f Bool } ``` Le paramĂ©trage par `f` permet d'utiliser le mĂȘme type pour representer de nombreuses variations : - `User Identity` est, Ă  un dĂ©tail prĂšs, un record `User` sans le paramĂ©trage, il contient donc une `String`, un `Int` et un `Bool`. - `User Maybe` est un `User` dont tous les champs sont des `Maybe`, c'est Ă  dire qu'ils peuvent avoir une valeur ou pas. C'est pratique en premiĂšre Ă©tape d'un systĂšme de validation, par exemple, on pourrait imaginer une fonction de validation `validation :: User Maybe -> Maybe (User Identity)`. - `User []` contient une liste pour chaque champs. C'est pratique pour faire du [Structure of Array](https://en.wikipedia.org/wiki/AOS_and_SOA). On pourrait mĂȘme imaginer une fonction `toArray :: [User Identity] -> User []` et `fromArray :: User [] -> [User Identity]` permettant de passer d'une liste d'utilisateur Ă  un utilisateur contenant des listes et inversement. Vous pouvez lire cet article sur les [functor functors](https://www.benjamin.pizza/posts/2017-12-15-functor-functors.html) pour plus de dĂ©tails sur cet usage. Il n'est pas aisĂ© de dĂ©river des instances pour `User`. En effet, pour pouvoir ĂȘtre affichĂ©, un `User f` doit pouvoir afficher chaque champs, c'est-Ă -dire : - `f String` pour le `nom` - `f Int` pour l'`age` - `f Bool` pour le `superUtilisateur` Ainsi, pour dĂ©river automatiquement l'instance de `Show`, il faudrait Ă©crire : ```haskell deriving instance ( Show (f String) , Show (f Int) , Show (f Bool) ) => Show (User f) ``` Cette Ă©criture est lourde, source d'erreur, peu gĂ©nĂ©rique et Ă©volue difficilement. Par exemple, si demain j'ajoute un champs `droitUtilisateur :: f Droits`, je devrais ajouter une clause `Show (f Droits)` Ă  toutes mes instances. Ce qui serait agrĂ©able c'est de pouvoir dire que si on peut afficher un `f b`, alors on peut afficher un `User f`. C'est ce qui est maintenant possible avec `QuantifiedConstraints` : ```haskell deriving instance (forall b. Show b => Show (f b)) => Show (User f) ``` Notez le `forall b. Show b => Show (f b)` qui signifie que quel que soit `b`, si on peut afficher `b`, alors si on sait afficher `f b` alors on sait afficher `User f`. Cette nouvelle extension apporte donc une trĂšs grosse simplification de cas d'instances complexes sur des types paramĂ©triques. ## Plus de tests d’exhaustivitĂ© GHC rĂ©alise des tests d'exhaustivitĂ© sur les `case`s et les appels de fonctions. Par exemple, le code suivant : ```haskell data Couleur = Rouge | Vert | Bleu phraseIdiote Rouge = "La lune est rouge, du sang a coulĂ© cette nuit" phraseIdiote Vert = "Les martiens sont tous verts" ``` Il manque le cas pour `Bleu` et le compilateur va s'en rendre compte: ``` :16:1: warning: [-Wincomplete-patterns] Pattern match(es) are non-exhaustive In an equation for ‘phraseIdiote’: Patterns not matched: Bleu ``` Cette capacitĂ© est maintenant Ă©tendue aux _guard_ et Ă  la syntaxe if Ă©tendue. Par exemple, le code suivant ne gĂšre pas le cas ou `b` est `False` : ```haskell -- si 'b' est True, retourne '1', sinon... ? foo b = if | b -> 1 ``` Ce code va maintenant gĂ©nĂ©rer un warning lors de la compilation. ## Plus de constant folding GHC effectue maintenant plus de _constant folding_ lors de la compilation. Par exemple, le code `(5 + 4*x) - (3*x + 2)` est Ă©quivalent Ă  `3 + x` tout en coĂ»tant deux multiplications, une addition et une soustraction de moins. ## Extension de `PartialTypeSignatures` `PartialTypeSignatures` est permet de ne pas expliciter tous les types dans une signature. On rappelle qu'en Haskell, les types sont infĂ©rĂ©s, donc il n'est presque jamais nĂ©cessaire de mettre de signature aux fonctions. Cependant cela permet de temps en temps de rendre le code plus lisible, de documenter ou de rendre la signature moins polymorphique. `PartialTypeSignatures` permet de ne prĂ©ciser que les types necessaires, les autres Ă©tant dĂ©duits via l'infĂ©rence de type. C'est particuliĂšrement utile avec certains types complexes ayant beaucoup de contraintes. Par exemple, la signature suivante qui cherche une valeur dans une liste: ```haskell -- 'Eq v' signifie que les valeurs dans la liste doivent pouvoir ĂȘtre comparĂ©es search :: Eq v => [v] -> v -> Bool ``` Pourrait ĂȘtre simplifiĂ©e en: ```haskell _ => [v] -> v -> Bool ``` C'est trĂšs pratique lors de l'usage de bibliothĂšques ayant de nombreuses contraintes. Depuis GHC 8.6, on peut maintenant utiliser le symbole `_` dans plus de contextes, et tout particuliĂšrement lors de la dĂ©finition d'une instance, par exemple `deriving instance Show v => Show [v]` qui dĂ©rive l'affichage de toute liste dont le type interne dĂ©rive l'affichage, pourra ĂȘtre Ă©crit `_ => Show [v]`. ## Heap View http://hackage.haskell.org/package/ghc-heap-view est maintenant integrĂ© Ă  ghci (le shell) et permet d'observer l'organisation en mĂ©moire des valeurs manipulĂ©es. https://patrickdoc.github.io/heap-view.html est une bonne introduction. Par exemple : ```haskell>>> import GHC.Exts.Heap>>> p = sum [1..10::Int] ``` Cette valeur n'est pas encore Ă©valuĂ©e : ```haskell>>> getClosureData p APClosure {info = StgInfoTable {entry = Nothing, ptrs = 0, nptrs = 0, tipe = AP, srtlen = 0, code = Nothing}, arity = 26827633, n_args = 0, fun = 0x0000004201996250, payload = []} ``` Chaque objet en mĂ©moire en Haskell commence par une table d'information `info` qui contient des informations pour le garbage collector et pour l'Ă©valuation. Ici on peut voir que le `tipe` est `AP`, une fonction Ă  appliquer. Nous pouvons forcer l'Ă©valuation de la valeur : ```>>> p 55>>> getClosureData p ConstrClosure {info = StgInfoTable {entry = Nothing, ptrs = 0, nptrs = 1, tipe = CONSTR_0_1, srtlen = 0, code = Nothing}, ptrArgs = [], dataArgs = [55], pkg = "ghc-prim", modl = "GHC.Types", name = "I#"} ``` Ici, on a une `ConstrClosure`, c'est Ă  dire un constructeur, un type concret. `55` apparaĂźt bien dans la liste des arguments et on peut voir que le constructeur est `I#`, le wrapper autour des entiers machine. Cela sera sans doute un trĂšs bon outil pour comprendre les comportements en mĂ©moire de certains algorithmes. On rappelle que le shell ghci vient avec `:sprint` qui permet d'observer moins de choses, mais au moins l'Ă©valuation paresseuse, les ĂȘlĂ©ments non Ă©valuĂ©s Ă©tant remplacĂ©s par `_` : CrĂ©ation d'une liste `l` contenant des chaines, tout est paresseux, d'ou `l = _` : ```haskell>>> l = ["Hello", "Youpi", "Yoda", "You", "Are", "Beautiful"]>>> :sprint l l = _ ``` RĂ©cupĂ©rer le premier Ă©lĂ©ment provoque l'Ă©valuation de seulement celui-ci : ```haskell>>> head l "Hello">>> :sprint l l = "Hello" : _ ``` Calculer la longueur de la liste provoque l'Ă©valuation de celle-ci, mais pas des sous Ă©lĂ©ments. Le premier est toujours Ă©valuĂ©. ```haskell>>> length l 6>>> :sprint l l = ["Hello",_,_,_,_,_] ``` `map head l` va rĂ©cupĂ©rer le premier caractĂšre de chacun des Ă©lĂ©ments de la liste, ce qui va provoque l'Ă©valuation partielle de ceux-ci : ```haskell>>> map head l "HYYYAB">>> :sprint l l = ["Hello",('Y' : _),('Y' : _),('Y' : _),('A' : _),('B' : _)] ``` La recherche d'un Ă©lĂ©ment `=="You"` est passionnante. "Hello" est dĂ©jĂ  Ă©valuĂ©, il ne change rien. `"Youpi"` doit ĂȘtre Ă©valuĂ© jusqu'au `i` pour rĂ©aliser que ce n'est pas la bonne valeur. `"Yoda"` pareil. `"You"` est totalement Ă©valuĂ© et l’exĂ©cution s'arrĂȘte : ```haskell>>> any (=="You") l True>>> :sprint l l = ["Hello",('Y' : 'o' : 'u' : 'p' : _),('Y' : 'o' : 'd' : _), "You",('A' : _),('B' : _)] ``` La crĂ©ation d'une liste paresseuse qui contient la longueur de tous les Ă©lĂ©ments de la liste ne change rien : ```haskell>>> l2 = map length l>>> :sprint l l = ["Hello",('Y' : 'o' : 'u' : 'p' : _),('Y' : 'o' : 'd' : _), "You",('A' : _),('B' : _)] ``` Mais l'observation du dernier Ă©lĂ©ment force l'Ă©valuation du dernier Ă©lĂ©ment de la premiĂšre liste : ```haskell>>> last l2 9>>> :sprint l l = ["Hello",('Y' : 'o' : 'u' : 'p' : _),('Y' : 'o' : 'd' : _), "You",('A' : _),"Beautiful"] ``` L'Ă©valuation paresseuse c'est formidable ;) ## MonadFail Jusqu'Ă  prĂ©sent, la classe `Monad` incluait une fonction `fail` qui Ă©tait utilisĂ©e en cas d'erreur de pattern dans une `do` notation. Par exemple, le code suivant, qui fonctionne pour toutes les `Monad`s : ```haskell f = do Just x <- anOperation pure x ``` Peut Ă©chouer, car il n'y a aucune garantie que la valeur matchĂ©e par `Just x` ne sera pas `Nothing`. Ainsi `Monad` incluait une fonction `fail` pour permettre de surcharger ce comportement. Il y a des cas oĂč l'implementation de `fail` a du sens. Dans les autres cas, `fail` est implementĂ© comme une exception. Ce qui veut dire que du code qui a l'air parfaitement anodin peut en fait planter lors de l'exĂ©cution, ce qui est inadmissible (avis d'un auteur ;) en Haskell oĂč on prĂ©fĂšre les erreurs statiques que les erreurs d'exĂ©cution. Il a donc Ă©tĂ© dĂ©cidĂ© de dĂ©placer `fail` de la classe `Monad` dans une super classe de `Monad`, appelĂ©e `MonadFail`. Ainsi, le code suivant, qui ne peut pas Ă©chouer : ```haskell f = do x <- anOperation pure x ``` Aura comme type `Monad m => m t` et est garanti sans exception. Alors que le code suivant, qui peut Ă©chouer : ```haskell f' = do Just x <- anOperation pure x ``` Aura comme type `MonadFail m -> m t` et le comportement en cas d'erreur dĂ©pend de la `Monad` utilisĂ©e. Par exemple : - La monad de liste va ignorer l'Ă©lĂ©ment associĂ© Ă  cet echec - Une opĂ©ration d'`IO` va rĂ©aliser une exception - Une monad type `Maybe` va renvoyer `Nothing`. La diffĂ©rence permet de faire apparaĂźtre la notion d'erreur possible dans le type. Bien sĂ»r, l'utilisation de `f'` dans un contexte `Monad` (et non `MonadError`) provoquera une erreur de compilation. L'honneur est donc sauf, moins d'erreur Ă  l'exĂ©cution, plus d'erreurs Ă  la compilation. ## `StarIsStar` `StarIsStar` est une nouvelle extension activĂ©e par dĂ©faut (et dĂ©sactivable avec `NoStarIsStar`) qui permet d'utiliser `*` comme symbole pour `Type` : le type de la plupart des types. Le but est Ă  terme de dĂ©sactiver cette extension afin de libĂ©rer le symbole `*` pour qu'il puisse ĂȘtre utilisĂ© comme symbole de multiplication au niveau du type. Ce n'est pas clair ? Il faut voir un type comme un ensemble de valeurs possibles. Par exemple, `Int` est l'ensemble des valeurs `0, 1, 2, 3, ..., -1, -2, -3, ...`. Il existe aussi un ensemble des types, qui s'appelle `Type` (ou `*` avec `StarIsStar`), cet ensemble contient `Int`, `Double`, ... et tous les types que vous allez crĂ©er. Cela a de l’intĂ©rĂȘt car on peut aussi dĂ©finir des types qui ne sont pas dans cet ensemble `Type`, c'est pratique pour faire des opĂ©rations au niveau du type. Et le problĂšme c'est que, au niveau du type, n'importe quel symbole Haskell peut ĂȘtre utilisĂ© pour implĂ©menter un opĂ©rateur, sauf `*` qui est un cas particulier pour reprĂ©senter `Type`, d'oĂč la motivation pour faire disparaĂźtre celui-ci afin de simplifier le compilateur et permettre son utilisation en tant qu'opĂ©rateur. Analyser le journal des modifications de LinuxFr.org ==================================================== Comme souvent avec les dĂ©pĂȘches sur une nouvelle sortie de GHC, celle-ci en profite pour montrer un exemple d'utilisation de Haskell dans la nature ! Il s'agit d'analyser syntaxiquement et de stocker de façon personnalisĂ©e les [changelogs](https://linuxfr.org/changelog) de ce site oĂč une entrĂ©e typique est gĂ©nĂ©rĂ©e sous cette forme : ``` commit ac34cc5d787f569a237d16a2ec8be994c2a37acc Author: Bruno Michel Date: Fri Aug 3 15:58:34 2018 +0200 Revert putting the phare on top of the logo and sidebar ``` Un modĂšle syntaxique correspondant en [BNF](https://fr.wikipedia.org/wiki/Forme_de_Backus-Naur) oĂč les chevrons reprĂ©sentent les terminaux : ``` Eol ::= Lettre ::= | Condensat ::= | | Eol Espace ::= | DĂ©but_e ::= < Email ::= DĂ©but_e | Lettre | <@> | <.> | <-> | <_> | Fin_e Fin_e ::= > Author ::= Lettre | Espace | Email | Eol Date ::= Lettre | | <:> | <+> | <-> | Espace | Eol Message ::= Lettre | Espace | <,> | <-> | <.> | | <:> | Eol Log ::= Condensat | Author | Date | Message | ``` Dans cette grammaire, on remarque d'emblĂ©e que tous les symboles, terminaux ou non, sont traitĂ©s de façon _ad hoc_, mĂȘme pour les cas ayant des standards les dĂ©finissant trĂšs prĂ©cisĂ©ment : adresses Ă©lectroniques, condensats et dates notamment. Cela vient d'une volontĂ© de simplifier au maximum l'exemple. La suite de cette section montrera comment s'y prendre en Haskell pour exprimer ce que `Log` exprime en BNF. Au sein de l'Ă©cosystĂšme Haskell, il existe plusieurs bibliothĂšques destinĂ©es Ă  traiter des grammaires comme celle-ci dont un certain nombre peuvent ĂȘtre regroupĂ©es sous l'appellation _"Parser Combinators"_. Ainsi de [parsec](http://hackage.haskell.org/package/parsec) qui a longtemps Ă©tĂ© la vitrine de telles bibliothĂšques, du fait d'un nom trop bien choisi entre autres choses (le `c` Ă©tant pour ... "combinators"). Il y a bien sĂ»r d'autres bibliothĂšques qui s'y prennent autrement, Ă  l'instar de `happy` et `alex` qui sont utilisĂ©es pour gĂ©rer GHC lui-mĂȘme. Notre exemple utilisera [megaparsec](http://hackage.haskell.org/package/megaparsec), une alternative de `parsec`. Dans les paragraphes qui vont suivre, les blocs de code seront tirĂ©s d'un fichier source appelĂ© `Depeche.hs`. Voici l'en-tĂȘte qui dĂ©clare les modules requis : ```Haskell module Depeche where import Data.Either -- base import Data.Foldable -- base import Data.Maybe -- base import Data.Void -- base import Prelude -- base import Text.Megaparsec -- megaparsec import Text.Megaparsec.Char -- megaparsec import Text.Read -- base import qualified Text.Megaparsec.Char.Lexer as Lx -- megaparsec import Data.Time -- time ``` Notons qu'Ă  part `megaparsec` et `time`, une seule bibliothĂšque livrĂ©e avec GHC fournit les modules requis par cet exemple. La derniĂšre dĂ©claration est dite « qualifiĂ©e » dans le sens oĂč toute entitĂ© venant de ce module devra ĂȘtre prĂ©cĂ©dĂ©e soit du nom de celui-ci, soit d'un prĂ©fixe Ă  prĂ©ciser (ici `Lx`) ; c'est pour Ă©viter la collision entre les espaces de noms de ce module et du reste des modules de `megaparsec` qui exposent des entitĂ©s homonymes. DĂ©finissons ensuite les types reprĂ©sentant une entrĂ©e du journal : ```Haskell data Author = MkAuthor { firstName :: String , lastName :: String , emailAddr :: String } deriving Show data ChangelogEntry = MkEntry { commitHash :: String , commitAuthor :: Author , commitDate :: UTCTime , commitMsg :: String } deriving Show ``` `Author` est un enregistrement Ă  trois champs, chacun Ă©tant une chaĂźne de caractĂšres. Une valeur de ce type est créée avec la fonction `MkAuthor`, automatiquement gĂ©nĂ©rĂ©e par le compilateur, et `deriving Show` permet de generer automatiquement la fonction `show` d'affichage. Une description similaire est applicable Ă  `ChangelogEntry`. `UTCTime` provient de `Data.Time` et reprĂ©sente une date. Passons ensuite aux parseurs qui auront tous ce format : ```Haskell type Parser = Parsec Void String ``` Ainsi, un parseur est un type composite qui met ensemble un type `Parsec`, ne se prĂ©occupe pas de personnaliser la gestion des erreurs (`Void`) et prend en entrĂ©e une chaĂźne de caractĂšres de type `String`. Tout ça est tout bonnement synonyme de `Parser` pour nous. `Parser a` est donc le type qui dĂ©crit un parseur d'une valeur de type `a`. Viennent ensuite les parseurs proprement dit, en commençant par les « sous-parseurs » : ```Haskell mixAlnumPunc :: [Char] -> Parser String mixAlnumPunc xs = some (alphaNumChar <|> oneOf xs) parseEmail, parseHash, parseLetters, parseMessage :: Parser String parseEmail = between (single '<') (single '>') (mixAlnumPunc "@.-_") parseHash = many (lowerChar <|> digitChar) parseLetters = many letterChar parseMessage = mixAlnumPunc " ,-./:" ``` Cet extrait ressemble furieusement Ă  la grammaire de dĂ©part ! Veut-on des `Lettre ::= | ` ? Tient : `many (lowerChar <|> upperChar)` ! Ou alors veut-on un tiret ou un espace soulignĂ©, `Tiret_ou_blanc_soulignĂ© ::= <-> | <_>` ? Et bien, voilĂ  : `single '-' | single '_'` ! Bref, avec les combineurs de parseurs, on dĂ©finit les Ă©lĂ©ments syntaxiques d'une maniĂšre trĂšs proche des notations usuelles. Il reste maintenant Ă  mettre ensemble ces petites briques pour bĂątir le parseur qui nous intĂ©resse. Si le ciment/sable/boue/gravier joue le rĂŽle de liaison entre plusieurs briques, les diffĂ©rents caractĂšres d'espacement jouent un rĂŽle similaire lorsqu'il s'agit d'assembler des Ă©lĂ©ments syntaxiques dans notre grammaire. Le sous-parseur suivant permet de passer outre tout espace Ă©ventuel avant de passer la main Ă  un autre parseur, ceci afin de ne garder que les briques : ```Haskell ignoreBlanks :: Parser a -> Parser a ignoreBlanks = Lx.lexeme (Lx.space space1 empty empty) ``` `Lx.lexeme` est une fonction permettant de dĂ©crire un parseur d'espace blanc. Elle peut aussi traiter les commentaires, ce que nous ignorons ici avec `empty`. Alors, oĂč est le principal parseur ? ```Haskell parseEntry :: Parser ChangelogEntry parseEntry = do _ <- ignoreBlanks (string "commit") theHash <- ignoreBlanks parseHash _ <- ignoreBlanks (string "Author:") theFirstName <- ignoreBlanks parseLetters theLastName <- ignoreBlanks parseLetters theEmail <- ignoreBlanks parseEmail _ <- ignoreBlanks (string "Date:") theDate <- ignoreBlanks parseUTC -- Voir plus bas theMessage <- ignoreBlanks parseMessage pure $ MkEntry theHash (MkAuthor theFirstName theLastName theEmail) theDate theMessage ``` Extraire des composants syntaxiques dans le changelog revient dans notre cas Ă  y aller mĂ©thodiquement de gauche Ă  droite et de haut en bas, une Ă©tape donnĂ©e se dĂ©roulant en commençant exactement lĂ  oĂč les prĂ©cĂ©dentes Ă©tapes se sont arrĂȘtĂ©es. La notation `do ... ← ...` sert Ă  sĂ©quencer les transitions pour faire intervenir un sous-parseur Ă  chaque fois. LĂ  oĂč le rĂ©sultat intermĂ©diaire est inutile, il est immĂ©diatement abandonnĂ© en l'envoyant dans le joker `_`. À la fin du parsing, tous les Ă©lĂ©ments sont rĂ©unis pour invoquer le constructeur `MkEntry`. Pour terminer, voici une utilisation de notre parseur : ```Haskell main :: IO () main = do fromStdIn <- getContents print $ find (\ x -> commitHash x == "a9bfdd7b21320ba18497519f4e2e471d1529c448") $ fromRight [] $ runParser (someTill parseEntry eof) "Changelog" fromStdIn ``` DĂ©cryptons un peu ce code condensĂ© : - `someTill parseEntry eof` est un parseur qui utilise `parseEntry` plusieurs fois jusqu'Ă  trouver une fin de fichier `eof`. - `runParser p "Changelog" fromStdIn` applique le parseur `p` sur l'entrĂ©e `fromStdIn` qui aura le nom de `Changelog` dans les messages d'erreurs. - Un parseur peut Ă©chouer et renvoyer `Left unMessageD'erreur` ou `Right leResultat`. La fonction `fromRight` nous permet d'ignorer l'Ă©chec et de le remplacer par une liste vide `[]`. - `find` va chercher dans cette liste une entrĂ©e avec un numĂ©ro de commit prĂ©cis. Pour compiler et expĂ©rimenter avec le programme dans le Shell, faire : ```Bash ghc Depeche.hs ./Depeche < 'linuxfr_changelog' ``` On pourra aussi l’exĂ©cuter directement sans compilation, ce code n'Ă©tant pas trĂšs demandeur en terme de ressource, il s’exĂ©cutera sans souci en mode interprĂ©tĂ© avec `runhaskell Depeche.hs < linuxfr_changelog`. En conclusion, Ă  partir d'une grammaire utilisant les notations habituelles comme BNF, les bibliothĂšques comme `megaparsec` permettent de dĂ©finir confortablement des analyseurs syntaxiques, le confort rĂ©sidant dans la similitude entre leur syntaxe Ă  la fois avec des syntaxes comme BNF et du langage Haskell lui-mĂȘme. **Addendum** Le parsing des dates utilise les fonctionnalitĂ©s de parsing de la libraire `Data.Time`. En voici le code : ```Haskell dateFormat = "%a %b %e %H:%M:%S %Y %Z" parseUTC :: Parser UTCTime parseUTC = do s <- manyTill anySingle eol parseTimeM False defaultTimeLocale dateFormat s ``` - `manyTill anySingle eol` parse jusqu'Ă  la fin de ligne tous les caractĂšres qui seront ensuite passĂ©s Ă  `parseTimeM`. - `parseTimeM ...` va parser la chaĂźne de caractĂšre en fonction de la chaĂźne de format spĂ©cifiĂ©e par `dateFormat`. On voit ici une des forces de Haskell qui est la possibilitĂ© de combiner les fonctions de `Data.Time` et de `Text.Megaparsec` ensemble. # Conclusion GHC 8.6.1 n’apporte pas forcement des nouveautĂ©s capables de motiver Ă  l’utilisation de Haskell, mais apporte son lot de petites amĂ©liorations qui, ensemble, rendent GHC plus pratique. GHC 8.8, [prĂ©vu pour mars 2019](https://ghc.haskell.org/trac/ghc/wiki/Status/GHC-8.8.1) devrait apporter son lot de nouveautĂ©s avancĂ©es au niveau du systĂšme de type, et fait quelques promesses concernant les performances du compilateur et du code gĂ©nĂ©rĂ©.

AltStyle ă«ă‚ˆăŁăŠć€‰æ›ă•ă‚ŒăŸăƒšăƒŒă‚ž (->ă‚ȘăƒȘă‚žăƒŠăƒ«) /