Greboca  

Suport technique et veille technologique

Aujourd’hui, les grandes entreprises et administrations publiques hésitent entre continuer à utiliser des logiciels propriétaires ou basculer vers les Logiciels Libres. Pourtant, la plupart des logiciels libres sont capables de bien traiter les données issues des logiciels propriétaire, et parfois avec une meilleur compatibilité.

C’est alors la barrière de la prise en main qui fait peur, et pourtant...

Les logiciels libres

L’aspect « Logiciel Libre » permet une évolution rapide et une plus grande participation des utilisateurs. Les aides et tutoriels foisonnent sur Internet ou sont directement inclus dans le logiciel lui-même.

Enfin, les concepteurs sont plus proches des utilisateurs, ce qui rend les logiciels libres plus agréable à utiliser et conviviaux.

Grâce à la disponibilité des logiciels libres, vous trouverez facilement des services de support techniques et la licence n’est plus un frein à l’utilisation de ces logiciels par votre personnel.

Notre support technique concerne essentiellement les logiciels libres, que ce soit sous forme de services ponctuels ou de tutoriels.

DLFP - Dépêches  -  GHC 9.2

 -  Novembre 2021 - 

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.

Sommaire

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 :

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 :

>>> 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 :

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:

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.

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 :

>>> 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 (et ses variantes pour d’autres type de lens, comme 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:

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 :

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. 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 et devrait réduire le préambule des fichiers dans un projet.

Le processus qui a permis de sélectionner ces extensions 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, `{mathjax}, 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 était plus petite. 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:

>>> 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:

>>> 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 :
>>> 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, mais cela reste très verbeux:

>>> 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:

>>> 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 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 :

>>> [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 :

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.

Commentaires : voir le flux Atom ouvrir dans le navigateur

par Guillaum, palm123, Ysabeau, tisaac, theojouedubanjo

DLFP - Dépêches

LinuxFr.org

Entretien avec GValiente à propos de Butano

 -  16 avril - 

GValiente développe un SDK pour créer des jeux pour la console Game Boy Advance : Butano.Cet entretien revient sur son parcours et les raisons (...)


Nouveautés d'avril 2024 de la communauté Scenari

 -  11 avril - 

Scenari est un ensemble de logiciels open source dédiés à la production collaborative, publication et diffusion de documents multi-support. Vous (...)


Annuaire de projets libres (mais pas de logiciels)

 -  9 avril - 

Les communs sont une source énorme de partage !S’il est plutôt facile dans le monde francophone de trouver des ressources logicielles (Merci (...)


Les enchères en temps réel, un danger pour la vie privée mais aussi pour la sécurité européenne

 -  7 avril - 

Les enchères en temps réel, ou Real-Time Bidding (RTB), sont une technologie publicitaire omniprésente sur les sites web et applications mobiles (...)


XZ et liblzma: Faille de sécurité volontairement introduite depuis au moins deux mois

 -  31 mars - 

Andres Freund, un développeur Postgres, s’est rendu compte dans les derniers jours que xz et liblzma ont été corrompus par l’un des mainteneurs du (...)