Le compilateur GHC Haskell en version 8.0.1

Posté par  (site web personnel) . Édité par palm123, patrick_g, Benoît Sibaud, claudex et Ontologia. Modéré par Ontologia. Licence CC By‑SA.
Étiquettes :
33
2
juin
2016
Programmation fonctionnelle

Le 21 mai 2016 est sortie la nouvelle version du compilateur Haskell GHC.

GHC est le compilateur principal du langage Haskell, disponible sur plusieurs plate-forme et sous une licence libre proche de la BSD.

Cette version succède à GHC 7.10.

Cette dépêche présente rapidement Haskell grâce à un exemple de code puis une sous-partie des nouveauté de GHC 8.0, le compilateur Haskell le plus utilisé. Comme on parle rarement d'Haskell ici, je vais le présenter avec une approche personnelle puis je me servirais de la liste des nouveautés pour présenter quelques fonctionnalités de ce langage de programmation.

Sommaire

Introduction, troll et digressions

J'ai découvert Haskell début 2015 avec l'envie de découvrir une nouvelle technologie totalement différente de mes pratiques habituelles. Je fais principalement du C++ quand j'ai besoin de performance et du Python le reste du temps. À l'époque j'avais un avis bien tranché et négatif sur le typage, n'y voyant qu'un outil de contrainte nécessaire pour les performances. J'ai donc essayé Haskell avec cette envie de me remettre en question : ce langage promettait du typage statique avancé.

Ce langage a changé ma manière de voir la programmation et l'ensemble de mes certitudes sur ce qui fait un bon outil de programmation. J'ai d'ailleurs réalisé une présentation sur Haskell à l'université de Lyon qui aurait dû s'intituler "Comment j'ai appris à ne plus m'inquiéter et à aimer les types".

Alors, rapidement, Haskell c'est :

  • un langage fonctionnel, ce qui se résume à une syntaxe et des "design patterns" pas habituels pour qui fait du C++. À l'usage on se fait plaisir à combiner ensemble des fonctions en fonctions de plus haut niveau.
  • un langage fonctionnel pur. Pas d'effet de bord par défaut. Les fonctions qui réalisent des effets de bords sont bien repérées car cela apparaît dans leur type. Au départ cela semble une contrainte et cela force à réfléchir un peu plus en amont de l'écriture du code. Mais au final j'ai l'impression que cela aide énormément à raisonner sur son code et à le maintenir.
  • un langage paresseux, c'est pratique de temps en temps, c'est embêtant le reste du temps. C'est discutable, mais j'apprécie.
  • un langage statiquement typé. Je m'attendais à devoir écrire des prototypes de fonction à rallonge, devoir me battre pour faire rentrer le type qui m'intéresse dans la fonction qui m'intéresse. Mais en fait il n'en est rien. En premier lieu Haskell a une inférence de type très robuste et au final on écrit rarement des types. Mon expérience est que le système de type d'Haskell ne m'a que rarement posé de problème.

Voila, au final Haskell est devenu pour moi un nouvel outil dans ma sacoche de développeur. Il remplace presque totalement Python car je le trouve plus sympa à utiliser, plus sûr du fait du typage statique et souvent bien plus performant, il me manque seulement certaines bibliothèques de l'écosystème Python scientifique. Il ne remplace pas C++ pour les performances malheureusement, mais s'en approche beaucoup. En pratique, si vous écrivez du code C++ naïf, le code Haskell sera équivalent. Si votre problématique en C++ sont les performances, alors vous pourrez écrire du code C++ bien plus efficace.

Exemple

Donc pour ceux qui n'en ont jamais fait et qui ont la flemme d'aller regarder les liens, voici un exemple d'Haskell. Il est discutable sur pleins de points comme tout exemple ;)

Celui-ci lit les mots du dictionnaire /usr/share/dict/french et les groupe par anagrammes. Pour cela on part d'un constat simple, les anagrammes ont les mêmes lettres triées. Par exemple "tortue" et "tourte" sont anagrammes l'un de l'autre car sort "tortue" == sort "tourte" == "eorttu". Pour cela, on va réaliser une structure associative (i.e. un dictionnaire) Map qui associe à la chaîne triée la liste de tous les anagrammes associés. Par exemple, on pourrait obtenir le dictionnaire suivant :

{
   "cehin" : ["chien", "niche"],
   "eorttu" : ["tortue", "tourte"]
}

Ainsi connaître les anagrammes de "chien" se résume à calculer sa clé d'anagramme en triant les lettres et à regarder dans la structure associée.

Voici le code que je commente tout de suite après :

import qualified Data.Map as Map          -- 1
import qualified Data.List as List        -- 2

filename = "/usr/share/dict/french"       -- 3

main = do                                 -- 4
  content <- readFile filename            -- 5

  let                                     -- 6
      wordList = lines content            -- 7
      anagramMap = mkAnagramMap wordList  -- 8

  print (getAnagrams anagramMap "chien")  -- 9
  print (getAnagrams anagramMap "écrire")
  print (getAnagrams anagramMap "bleurk")

key word = List.sort word                -- 10

getAnagrams anagramMap word = Map.lookup (key word) anagramMap -- 11

mkAnagramMap wordList =                  -- 12
  let
    keyValue = map (\x -> (key x, [x])) wordList       -- 13
  in
    Map.fromListWith (++) keyValue                     -- 14

En premier lieu on observe qu'il n'y a aucune déclaration de type, pas de syntaxe verbeuse pour les faire apparaître. C'est grâce à l'inférence de type. Très souvent les types ne sont pas nécessaires et le compilateur se débrouille seul, mais on peut les ajouter explicitement si on le désire pour documenter ou pour limiter le polymorphisme. On peut exécuter ce code et obtenir :

Just ["niche","chien"]
Just ["r33crie","33crire","33crier"]
Nothing

Rassurez vous, Haskell gère très bien l'Unicode, mais pas pour l'affichage dans une structure imbriquée. Notez que pour le mot "bleurk", il n'existe pas d'anagramme, et la fonction renvoie Nothing au lieu de Just ....

  • Les lignes 1 et 2 importent les modules de manipulation de liste et de map.

  • La ligne 3 est juste une constante de type chaîne de caractère

  • La ligne 4 est le début de la fonction main. C'est une fonction particulière, toutes les lignes qui la composent (sauf celles avec let) sont des effets de bords.

  • La ligne 5 lit le contenu du fichier dans la variable content. Petite subtilité, on utilise <- au lieu de = pour marquer l'application d'un effet de bord, le égal étant réservé à l'égalité et non pas à l'affectation.

  • La ligne 6, bloc let dit que cette zone est sans effet de bord.

  • Ligne 7, on sépare le contenu du fichier en une liste avec un élément par ligne (bref, une liste de mots). Ici la fonction de séparation par retour à la ligne s'appelle lines, il existe une fonction words pour séparer par espaces blancs et une fonction split plus générique.

  • Ligne 8, on crée la structure qui associe les mot triés à leurs anagrammes en appelant la fonction mkAnagramMap.

  • Ligne 9, on affiche les anagrammes avec la fonction getAnagrams.

  • Ligne 10, on définit une fonction utilitaire key qui se contente de renvoyer le paramètre trié. Une chaîne de caractère peut être représentée par une liste en Haskell, c'est le choix que j'ai fait pour les besoins de cet exemple.

  • Ligne 11, définition de getAnagram qui regarde dans la structure si la clé y est. Pas d'opérateur pour l'accès aux cases d'un dictionnaire en Haskell, il faut utiliser Map.lookup qui renvoie Nothing si la clé n'existe pas ou Just value si elle existe.

  • Ligne 12, on crée la structure qui contient les anagrammes. Cette création se fait en deux parties.

  • Ligne 13, on crée une liste qui à chaque mot associe sa clé et le mot dans une liste. Exemple : ["chien", "tortue"] -> [("cehin", ["chien"]), ("eorttu", ["tortue"])]. Cette association est faite car nous allons créer notre structure à partir d'une liste d'association clé / valeur. Pour cela on utilise la fonction map qui exécute une fonction anonyme \x -> (key x, [x]) sur chaque valeur de wordList. On note que la valeur est encapsulée dans une liste car on veut crée une structure d'association entre une chaîne et une liste de chaînes.

  • Ligne 14, on convertit notre liste clé / valeur en Map en utilisant la fonction Map.fromListWith. Les conflits sont réglés par la fonction (++) qui réalise la concaténation des listes d'anagrammes en conflit.

Nouveautés de GHC 8.0.1

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

DeriveAnyClass

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

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

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

import GHC.Generics
import Data.Serialize

Maintenant nous allons définir notre type d'arbre binaire :

data Tree t = Node t (Tree t) (Tree t) | Leaf
                     deriving (Show, Serialize, Functor, Foldable, Generic)

Ici pas mal de choses à expliquer. data Tree t signifie que nous créons un type Tree polymorphe paramétré par un type arbitraire t. C'est l'équivalent du C++ template<typename T> class Tree {}. Cette arbre peut avoir deux représentations, équivalentes d'une union C, mais typée :

  • soit un Node associé à trois valeurs : une valeur arbitraire de type t, et deux sous arbres Tree t.
  • soit une feuille Leaf.

Ainsi un arbre binaire vide sera représenté par Leaf. Alors que l'arbre binaire suivant :

          5
         / \
        3   6
       / \ / \
      0  ..   .
Sera représenté par :
 Node 5
    (Node 3
      (Node 0 Leaf Leaf)
      Leaf
    )
  (Node 6 Leaf Leaf)

Pour finir, la clause deriving (Show, Serialize, Functor, Foldable, Generic) liste les comportements par défaut que nous voulons automatiquement dériver :

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

Soit :

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

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

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

-- Foldable
> maximum tree
6
> minimum tree
0
> sum tree
14

Les nouveautés de GHC 8.0 vont permettre de rendre encore plus générique et facile ce type de comportement par défaut.

PatternSynonyms

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

data Date = Date Int Int Int

Et une fonction qui a une liste de dates spéciales associe une chaine :

evenements date = case date of
   Date 1 1 year -> "Premier jour de l'année " ++ (show year)
   Date 24 12 _ -> "Noél"
   Date _ 7 _ -> "Le mois de Juillet"
   Date 6 6 _ -> "Joyeux anniversaire"
   _ -> "Jour ininteressant"

Ici le pattern Date permet de réaliser des comparaisons absolues ou partielles (_ est le joker) et permet aussi d'affecter des variables comme ici year. Le souci c'est que dans certains cas, les patterns par défaut ne sont pas assez expressifs et deviennent trop complexe. Il est possible de mettre en place des synonymes :

{-# LANGUAGE PatternSynonyms #-}

pattern Year x <- Date _ _ x
pattern Noel <- Date 24 12 _
pattern Month x <- Date _ x _
pattern Birthday x y <- Date x y _
pattern FirstOfYear x = Date 1 1 year

Permettant de réécrire le code précédent :

evenements date = case date of
   FirstOfYear year -> "Premier jour de l'année " ++ (show year)
   Noal -> "Noel"
   Month 7 -> "Le mois de Juillet"
   Birthday 6 6 -> "Joyeux anniversaire"
   _ -> "Jour inintéressant"

Ceci permettant de rendre le code bien plus lisible localement. GHC 8.0 apporte donc son lot de nouveautés sur le support des patterns synonymes avancés.

En vrac

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

Base est la bibliothèque standard associée à GHC et est maintenant disponible en version 4.9.0.0. Changements majeurs :

  • Certaines fonctions comme error and undefined affichent la pile d'appel lorsque utilisée, cela est plus facile pour corriger les bugs.
  • Ajout de Data.List.NonEmpty qui représente des listes non vides. Ce type est pratique si lors de votre conception vous savez que vos listes ne peuvent être vides. Cela permet de simplifier et de réduire les risques d'erreur car certaines fonctions, comme maximum, qui ne sont pas définies sur les listes vides, peuvent être appelées sans crainte.
  • Ajout Data.SemiGroup qui représente l'ensemble des types possédant une opération de réduction mais pas d'élément neutre. J'en parlais dans ce journal sur le tri en Haskell. Les Monoids sont les types qui admettent une opération de réduction et un élément neutre, comme la somme pour les entiers, qui admet 0 comme élément neutre, ou la concaténation de chaînes de caractère, qui admet la chaîne vide comme élément neutre. Les entiers admettent l'opération de réduction minimum, mais il n'existe pas d'élément neutre, ainsi ce ne sont pas des Monoids mais ce sont des SemiGroups.

Ces deux nouvelles bibliothèques sont plutôt représentatives de la philosophie de typage qui existe en Haskell. Là où dans de nombreux langages on utiliserait naïvement un entier pour stocker le calcul d'un minimum, en Haskell nous utiliserons un type Min Int. Celui-ci a l'avantage de ne permettre que des calculs de minimum, là ou l'entier permet de nombreuses choses comme l'addition, la soustraction, le modulo, … En Haskell on essaye de restreindre les types à leur fonction et rien de plus. Cela permet de documenter par le type et en second lieu on évite des erreurs futures en ne rendant pas des opérations fausses disponibles. Par exemple, cela a du sens de faire une addition entre deux monnaies identiques, mais pas de sens de multiplier celles-ci. Une modélisation qui permet la multiplication permet un jour l'usage de celle-ci et ainsi l'introduction d'un bug.

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

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

Digressions

GHC 8.2 est prévu pour Avril 2017 d'après la page de Status.

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

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

Aller plus loin

  • # Erreur dernier lien

    Posté par  . Évalué à 1.

    Le dernier lien est erroné : il manque juste un -l à la fin apparemment.

  • # Merci pour cette annonce

    Posté par  . Évalué à 2.

    En contrepartie, écrire du code Haskell sans certaines extensions est assez déprimant et on est arrivé à une situation où GHC est le seul compilateur capable de faire tourner la plupart des codes disponibles.

    Ce qui est un des points qui me gêne chez Haskell (mais j’aime quand même le langage par ailleurs).
    Un standard qui est là seulement pour faire joli c’est déjà moyen, mais en plus le fait qu’une grosse majorité du code qui dépends d’extension GHC rend très difficile (voir impossible) l’apparition d’un compilateur alternatif.

    Je vous encourage aussi à regarder l'outil Liquid Haskell qui propose de la vérification statique de prédicats sur votre code Haskell et qui est un bon complément au typage pour rendre son code encore plus robuste.

    Ça me fait un peu penser à SPARK pour Ada.

    • [^] # Re: Merci pour cette annonce

      Posté par  (site web personnel) . Évalué à 2.

      Ce qui est un des points qui me gêne chez Haskell (mais j’aime quand même le langage par ailleurs).
      Un standard qui est là seulement pour faire joli c’est déjà moyen, mais en plus le fait qu’une grosse majorité du code qui dépends d’extension GHC rend très difficile (voir impossible) l’apparition d’un compilateur alternatif.

      Je ne connais aucune technologie qui actuellement garantie la portabilité entre différents compilateurs / système / … En C++ le standard n'est pas forcement respecté et avoir du code portable entre les différents compilateurs n'est pas si triviale que ça. En Python, il n'y a pas de standard et il existe plusieurs implémentation différentes.

      Alors si demain quelqu'un veut faire un nouveau compilateur Haskell, il lui faudra une sacrée bonne raison et si elle est suffisamment valable, je suis près à limiter mon usage de certains extensions pour assurer la portabilité.

      On pourrait aussi se dire que le standard Haskell pourrait évoluer pour intégrer ces extensions, mais au final cela ne ferait que repousser le problème qui est qu'il devient difficile avec toute technologie complexe de repartir de zéro.

      • [^] # Re: Merci pour cette annonce

        Posté par  . Évalué à 7.

        Je ne suis pas convaincu par ta réponse. Il existe des langages qui ont plusieurs implémentations robustes et utilisées; c'est le cas de C++, avec au moins GCC et Clang, de Javascript (v8, TraceMonkey, Chakra, JavaScriptCore), Scheme, Common Lisp, Prolog. Il existe des langages qui sont mono-implémentation, soit parce qu'ils ont toujours été conçus comme cela, la spécification évolue en tandem avec l'implémentation (OCaml, Python, Ruby, F#, Racket, PHP, Rust, Flash, etc.), soit parce qu'en pratique quasiment tout le monde utilise la même implémentation qui diverge du standard (Haskell, PDF, gmake).

        Cette notion peut évoluer dans le temps; Haskell a commencé comme un langage multi-implémentations (et Hugs reste vaguement utilisable), mais c'est aujourd'hui un langage mono-implémentation dans la pratique. Tu ne peux pas comparer avec C++ comme tu le fais; il y a une différence forte entre "il y a des incompatibilités qui font que toutes les implems ne vont pas gérer tous les projets" et "en pratique le code écrit par les gens aujourd'hui utilise toujours toujours la même implémentation".

        Le processus de standardisation de Haskell est quasiment inexistant aujourd'hui, tous les ans les gens essaient de le relancer et pour l'instant ça n'a pas abouti à grand chose—au mieux une reconnaissance du statu quo avec un nouveau tampon "maintenant c'est standard" sur ce que fait GHC. La communauté Haskell est très vivante, mais pour l'instant c'est avec une implémentation unique; ça pourra re-changer à l'avenir mais ça n'en prend pas le chemin—par exemple il y aurait pu avoir une autre implémentation visant le Javascript, mais en pratique ça ne marche pas bien et GHCJS va sans doute l'emporter.

        • [^] # Re: Merci pour cette annonce

          Posté par  (site web personnel) . Évalué à 3.

          Je comprend ta réponse.

          C'est juste que je ne vois pas vraiment cela comme un problème. Au final on serait en train de reprocher à GHC de faire avancer un langage au détriment d'un standard qui stagne. Il n'y aurait pas eu de standard et une unique implémentation, comme c'est le cas pour beaucoup de langages que tu cites, cela n'aurait pas posé de problème.

          • [^] # Re: Merci pour cette annonce

            Posté par  . Évalué à 5.

            Je suis d'accord sur le fait que ce n'est pas vraiment gênant, et que ça peut même un avantage. Je pense que c'est un de ces trucs de communication où les gens ont tendance à imaginer des problèmes où il n'y en a pas, exagérer l'importance de certains trucs "flashy" ou diminuer l'importance de vrais problèmes. À mon avis le mieux dans ces cas-là c'est d'assumer l'état de fait et de contre-communiquer sur ce qu'on trouve important ou intéressant—ta dépêche étant un exemple qui va dans le bon sens.

            Dans l'absolu une implémentation unique peut être problématique. Par exemple, dans le cas où les auteurs de l'implémentation sont soupçonnés de ne pas forcément avoir à cœur l'intérêt commun (par exemple: Oracle avec Java, Microsoft avec leur standard bureautique douteux, Microsoft avec .NET (même s'ils sont particulièrement gentils dernièrement)), quand l'implémentation de référence est propriétaire (DMD pour D, Shen, etc.) ou a des processus de développement trop opaques ou pas assez ouverts (ça fut le cas pour Caml pendant un temps, ça ne l'est plus), ou quand cela réduit la liberté des utilisateurs (les services centralisés qui accumulent les données). Il se trouve qu'aucune de ces critiques ne s'applique à GHC—je pense que le pire qu'on puisse dire de GHC c'est que c'est un vieux, gros projet et que c'est parfois un peu lourd à faire évoluer. Mais une personne qui ne connaît pas bien peut repérer un schéma qui la dérange sans avoir les informations en main pour évaluer si cette craine est justifiée.

            Enfin, il y a des gens qui critiquent légitimement l'abus d'extension chez certains auteurs de code. Les mille-feuilles d'extension, c'est un choix de conception qui vient directement de la monoculture GHC et qui a des avantages et des inconvénients. La raison d'être de fond de cette discussion à mon avis, c'est de créer une pression pour décourager les développeurs d'utiliser trop les extensions avancées du langage, car ça peut rendre le code fragile ou difficile à comprendre pour les néophytes. Il y a des abus, la plupart des gens ont du mal à se retenir de jouer avec leurs jouets, et le fait de râler sur les extensions dans l'absolu peut avoir un effet bénéfique en faisant prendre conscience aux gens que ce n'est pas toujours la meilleure idée.

            • [^] # Re: Merci pour cette annonce

              Posté par  (site web personnel) . Évalué à 1.

              Dans l'absolu le langage bénéficierait tout de même d'une standardisation un peu plus poussée je pense, et sans doute qu'une plus large communauté s'y intéresserait, mais ce n'est pas l'objectif du projet, en tous cas pas à moyen terme.

              • [^] # Re: Merci pour cette annonce

                Posté par  . Évalué à 3.

                Peux-tu expliquer en quoi le fait de faire plus de standardisation ferait qu'une "plus large communauté s'y intéresserait (sans doute)" ? Je ne dis pas que je ne suis pas d'accord (je pense que les gens s'intéressent parfois aux choses pour des raisons irrationnelles, et que ça pourrait en effet arriver) mais je me demande si tu as des raisons différentes en tête.

                En tout cas il me semble clair qu'il y a des communautés qui n'ont pas de processus de standardisation et qui ont une communauté très large (beaucoup plus large que Haskell)—par exemple Python, Ruby, Node.js, etc. Il y a aussi des grosses communautés qui sont standardisées et multi-implémentation depuis longtemps (C, C++), mais il me semble que c'est plutôt l'exception que la norme.

                • [^] # Re: Merci pour cette annonce

                  Posté par  (site web personnel) . Évalué à 2.

                  Bonjour, salut et éventuellement je peux tenter une explication. En fait, je m'étais fait la réflexion qu'un frein à l'adoption d'un langage est l'absence de standard explicite. Parce que si tout le monde fait son truc dans son coin, on arrive pas à avoir une masse critique d'utilisateurs qui fera que la communauté prendra son essor.

                  Tu parles de la communauté python, je suis vraiment désolé, mais je suis en désaccord avec ce que tu dis :

                  il y a des communautés qui n'ont pas de processus de standardisation et qui ont une communauté très large (beaucoup plus large que Haskell)—par exemple Python, Ruby, Node.js, etc.

                  Python au contraire me semble faire des efforts considérables pour atteindre un très haut degré de standardisation. Les infinies précautions prisent par le consortium pour passer de python 2 à python 3, le « preferably one way », ce sont des prises de position qui incitent à penser que python, c'est un processus de maturation de qualité. Les acteurs industriels dans python sont bien plus nombreux¹ que dans GHC par exemple².

                  Pourquoi la standardisation est-elle intéressante pour développer une large communauté ? Parce qu'une infrastructure logicielle, on n'a pas forcément envie de la voir voler en éclat à chaque mise à jour du compilateur. On sait tous que ce qui coûte cher dans un soft, c'est la maintenance. L'erreur classique dans un cycle de développement est de négliger le coût de la maintenance/évolution initialement, pour le voir ensuite exploser parce que les décisions initiales ont été incorrectes. Il suffit d'aller un peu trop vite pour voir la facture devenir ensuite beaucoup plus salée³.

                  D'après les quelques échanges que j'ai eu sur IRC, Haskell ne vise pas une stricte standardisation. La communauté et le noyau des développeurs souhaite incrémenter tranquillement les fonctionnalités du compilateur, tout en restant dans l'esprit langage fonctionnel pur (c'est un des seuls langages avec ce degré de pureté fonctionnelle pourrait-on dire). Forcément cela ralentit l'adoption massive mais j'ai l'impression que de plus en plus de personnes se tournent vers ce langage⁴. Si tu lis l'anglais je te conseille ce lien.

                  1. Python vient d'être adopté comme langage de programmation officiel pour la nouvelle mouture du CAPES de mathématiques (site officiel).
                  2. Ni Ruby ni Node.js (que je connais mal en passant) ne me semblent avoir la même visée ni la même généricité que python
                  3. Ça me fait penser à l'EPR de Flamanville.
                  4. Cette page est tout de même assez éloquente sur les potentialités du langage.
                  • [^] # Re: Merci pour cette annonce

                    Posté par  . Évalué à 6.

                    Dans cette discussion nous utilisons la notion de langage standardisé dans deux sens, un faible (standard sur le papier) et un fort (standard dans les faits):
                    - sens faible (sur le papier): il existe une spécification du langage annoncée comme un standard multi-implémentations
                    - sens fort (dans les faits): la possibilité d'avoir plusieurs implémentations conformes et compatible est utilisée en pratique, et la communauté est sainement répartie entre plusieurs implémentations

                    Python n'est un standard ni sur le papier, ni dans les faits. Il n'y a pas de standard multi-implémentation, la référence du langage évolue en tandem avec l'évolution de l'implémentation principale, Regarde le guide How to contribute to Python sur le site officiel du langage par exemple, il mélange complètement le concept de Python, le langage et CPython, l'implémentation "officielle" (c'est-à-dire, la seule qui définit le langage). Python a une "Language Référence" qui est relativement bien écrite, et pourrait jouer un rôle de spécification, et un processus d'évolution communautaire (les PEPs) bien rodé, mais ça n'en fait pas un langage standardisé.

                    (Haskell a un standard sur le papier, Haskell 98, qui a été un standard dans les faits au début (à partir de 1998), mais ne l'est plus aujourd'hui. Haskell est plus standardisé que Python; il a réellement été conçu par un consortium d'implémenteurs de langages paresseux ayant décidé de se mettre d'accord. Les évolutions du langage Python sont décidées par les mainteneurs d'une unique implémentation.)

                    Il y a des gens qui travaillent à des implémentations alternatives de Python (Jython, Pypy, Pyston, Stackless Python…), mais celles-ci doivent suivre les choix de l'implémentation principale. En particulier, alors que respecter la sémantique du langage de surface est relativement faisable, être compatible avec la FFI de l'implémentation officielle est très difficile—ce qui limite beaucoup les possibilités de ces implémentations secondaires. On peut se demander si la même situation se serait produite dans un langage standardisé, où plusieurs implémentations sont utilisées dès le départ.

                    Pourquoi la standardisation est-elle intéressante pour développer une large communauté ? Parce qu'une infrastructure logicielle, on n'a pas forcément envie de la voir voler en éclat à chaque mise à jour du compilateur.

                    Un argument curieux au sein d'une comparaison de Python et Haskell, puisque la communauté Python a un gros problème de fracture entre les versions 2.x et 3.x du langage, alors que la communauté Haskell passe relativement rapidement aux nouvelles versions de GHC.

                    • [^] # Re: Merci pour cette annonce

                      Posté par  . Évalué à 2.

                      Dans cette discussion nous utilisons la notion de langage standardisé dans deux sens, un faible (standard sur le papier) et un fort (standard dans les faits):
                      - sens faible (sur le papier): il existe une spécification du langage annoncée comme un standard multi-implémentations
                      - sens fort (dans les faits): la possibilité d'avoir plusieurs implémentations conformes et compatible est utilisée en pratique, et la communauté est sainement répartie entre plusieurs implémentations

                      Contrairement à l'Anglais, le Français a une façon de distinguer les deux : on parle de norme lorsqu'il s'agit d'avoir un groupe de gens qui décident de spécifier quelque chose de façon formelle (exemples : AFNOR, ISO, ANSI, etc.), et on parle de standard lorsqu'il s'agit d'une « norme de fait » (par exemple : Perl, Python, Ruby, etc.).

                      Donc je propose d'utiliser ces termes pour le reste de la discussion s'il y a besoin de faire une distinction. ;)

                      • [^] # Re: Merci pour cette annonce

                        Posté par  . Évalué à 3.

                        on parle de standard lorsqu'il s'agit d'une « norme de fait » (par exemple : Perl, Python, Ruby, etc.).

                        Python a les PEP…

                        Tous les contenus que j'écris ici sont sous licence CC0 (j'abandonne autant que possible mes droits d'auteur sur mes écrits)

                    • [^] # Re: Merci pour cette annonce

                      Posté par  . Évalué à 2.

                      J'oubliais :

                      Python n'est un standard ni sur le papier, ni dans les faits. Il n'y a pas de standard multi-implémentation, […]

                      C'est faux. Par exemple, Python 2.x possède plusieurs implémentations. Il y a au moins celle de référence, et aussi IronPython (une implémentation en .Net).

                      Une norme est un document qui définit formellement comment implémenter une certaine technologie. Un standard est « simplement » quelque chose qui est en pratique utilisé par un grand nombre de gens. Comme c'est (le plus souvent) une technologie utilisée en grande majorité pour une implémentation spécifique (par exemple : Python, Ruby, Perl), cette implémentation de référence devient le standard.

                      […] la référence du langage évolue en tandem avec l'évolution de l'implémentation principale

                      IronPython (qui semble arrêté depuis que Python 3 devient de plus en plus utilisé, c'est dommage) a implémenté Python jusqu'en version 2.7.

                      Autre exemple : Java n'est pas une norme (Sun/Oracle ont toujours refusé de passer par un institut de normalisation), mais ont produit un ensemble de benchmarks/outils de vérification pour garantir qu'une implémentation autre que celle de référence est conforme à la spécification du langage. Java est donc bien un standard, mais pas une norme.

                      • [^] # Re: Merci pour cette annonce

                        Posté par  . Évalué à 3.

                        Je suis au courant d'implémentations alternatives de Python (je les mentionne dans mon message ci-dessus), mais je conteste le fait qu'on puisse parler de standard multi-implémentation dans ce cas. Quand tu demandes au gens dans la communauté, la définition de Python c'est "ce que fait CPython". Tu n'as pas:

                        • une masse critique qui utilise une autre implémentation que CPython pour lui assurer la longévité en pratique (à part peut-être Jython ?)
                        • une tentative d'évolution de l'écosystème pour rendre la vie plus facile aux implémentations alternatives (tout le boulot sur une FFI efficace par exemple doit être tiré à bout de bras par Pypy et Pyston)
                        • de discussion saine entre les différents implémentations sur comment faire évoluer le langage (comme c'est le cas pour Javascript ou C++ ou Ada par exemple)
                        • ou même une façon raisonnable pour les implémentations alternatives de tester leur conformance comme "une implémentation de Python" (Java, Javascript…)

                        Pour moi pour pouvoir parler d'un standard, il faut une définition/description du langage qui soit assez précise pour servir de base à une implémentation (une implémentation n'est pas une description, c'est plutôt un seau de cambouis qui parfois fait des trucs), et que les différents acteurs font des efforts pour respecter. Ce n'est pas une question formelle de normalisation ISO/Oasis/machin (on vu avec ooxml que c'est ouvert aux abus et ça suffit pas), mais une question sociale de la façon dont fonctionne une communauté autour d'un langage. Python (ou Ruby, Perl) n'atteint pas ce niveau; c'est un langage défini par une seule implémentation, avec quelques implémentations alternatives qui courent derrière.

                        Et je trouve que mentionner IronPython enlève du poids à ton argument au lieu d'en ajouter. Sérieusement, connais-tu quelqu'un qui a utilisé IronPython pour de vrai, sans avoir été payé par Microsoft pour cela ?

                        Quand ils ont lancé .NET, Microsoft avait tout intérêt à montrer sa technologie et à donner l'image d'une plateforme ouverte aux langages qui ne sont pas sous son contrôle direct. Du coup ils ont fait le choix intelligent de financer le développement d'implémentations alternatives de langages à la mode, genre IronPython, ou un backend Scala qui a été mentionné pendant un temps (avant d'être abandonné parce que tout le monde s'en fichait et que ça faisait de la maintenance), etc. (Pour F# c'est différent, les chercheurs avaient envie de se faire plaisir en faisant du ML, ensuite ils ont eu l'idée de faire un clone de OCaml qui était utilisé en interne à MS sur des projets intéressants, et enfin l'équipe Caml leur a conseillé de partir sur un vrai langage à part entière, ce qui a très bien marché.)

                        Mais ces backends .NET de langages plus ou moins à la mode (IronPython, IronRuby, IronLisp->IronScheme…) étaient mort-nés: pas de vraie communauté derrière (en plus à l'époque MS ne savait pas vraiment faire du libre), des difficultés sur la compatibilité (pour Python, pas de vrai standard tout ça, trop de code C à porter) ou sur les performances (IronRuby: en fait une VM pensée pour des langages pas trop follement dynamiques a du mal à faire tourner le monkey-patching de façon efficace, etc.). Dans les années 2010, Microsoft a décidé de laisser tomber ces projets qui n'avaient jamais percé, et je ne crois pas qu'ils aient jamais eu une vraie adoption entre temps—les gens mettent du temps à migrer vers des implémentations alternatives, ça va souvent plus vite mais ça marche aussi parfois moins bien, et ça c'est grave.

                        • [^] # Re: Merci pour cette annonce

                          Posté par  . Évalué à 4.

                          J'avais écrit une grosse réponse, mais je vais essayer de la réduire à un ou deux paragraphes.

                          1. Je me suis trompé dans mon message précédent, il existe bien une implémentation pour Python 3. Il y a donc un ensemble de gens qui ont un intérêt à pouvoir faire interagir l'écosystème .Net avec Python, et je ne vois pas pourquoi on devrait les ignorer. Tout comme les gens qui écrivent du Scala ou du Clojure se servent de la JVM et de bibliothèque standard de Java pour réutiliser des briques de base déjà bien testées. Que Microsoft ait commencé cette initiative pour attirer plus de développeurs n'est pas pertinent pour la discussion : il existe bel et bien une implémentation complète du langage autre que CPython. Que toi tu n'en aies pas l'utilité est un autre problème.
                          2. La grande majorité des langages réellement utilisés n'ont pas de norme associée à ma connaissance (évidemment je peux me tromper) : Java a un comité « informel » (± les gros acteurs industriels que Sun/Oracle doivent accepter car sinon ils se tourneraient vers d'autres technologies), OCaml, Scala, Python, Ruby, Perl, etc. Par contre, il existe des spécifications pour le langage et ce qui doit être considéré comme la bibliothèque standard associée (voir par exemple ici pour Python 3).
                          3. La plupart de ces langages n'ont qu'une implémentation de référence. Il y a peut-être des ports ailleurs, mais souvent seule l'implémentation de référence est compétitive en termes de performance, ou pas complète en termes de bibliothèque standard par exemple 1. Même Java, qui n'est pas normé mais qui est clairement un standard, n'a que 2 vraies implémentations « compétitives » : OpenJDK et l'implémentation d'IBM (l'implémentation GNU est intéressante d'un point de vue académique mais je ne l'ai jamais vue déployée en industrie). Dans le cas de Python, comme l'implémentation de référence est libre et que c'est celle qui est utilisée en pratique, il existe un moyen « organique » de faire évoluer le langage ou la bibliothèque standard : s'inscrire sur la liste de diffusion, participer activement à l'évolution du langage ou de la bibliothèque en proposant des patches, etc. Parmi les langages qui ont une vraie norme, je ne connais que C++ qui permet à un individu (dans le sens qu'il ne s'exprime pas pour sa boite, ni n'est nécessairement soutenu par elle) de pouvoir réellement influer sur l'évolution du langage.

                          1. Notons que dans le cas de IronPython, comme c'est la VM de .Net qui est utilisée, l'implémentation n'est pas forcément mauvaise en termes de perf… 

                          • [^] # Re: Merci pour cette annonce

                            Posté par  . Évalué à 3.

                            La différence entre Scala/Clojure et IronPython est que la JVM est leur environnement "natif" (de naissance): tout le monde les utilise sur ça. Pour IronPython, le runtime dotnet est un port alternatif, et il n'a pas beaucoup d'utilisateurs à ma connaissance. Dans l'absolu je suis prêt à acheter que les gens qui ont un soft sur .NET et qui veulent ajouter du scripting en python utiliseraient IronPython: ça semble raisonnable comme choix technique. Mais as-tu des exemples concrets d'adoption ? Pour aller dans ton sens, j'imagine que le fait que c'est le seul des Iron* encore maintenu après que Microsoft ait coupé les fonds est plutôt bon signe. Mais, encore une fois, pour moi il n'y aura un statut d'implémentation alternative "viable" quand quand les gens qui font évoluer le langage prendront en compte aussi les besoins et propositions d'IronPython. (Et à mon avis c'est plus près d'arriver pour Pypy, mais on verra). En tout cas ce n'est pas le cas aujourd'hui.

                            La grande majorité des langages réellement utilisés n'ont pas de norme associée à ma connaissance [..] Par contre, il existe des spécifications pour le langage et ce qui doit être considéré comme la bibliothèque standard associée.

                            Je suis d'accord, c'est ce que j'ai écrit dans mes messages précédents. Mais est-ce que la référence Python est assez complète pour implémenter une autre implémentation conforme en pratique ? Il me semble que la réponse est non à cause de la dépendance très forte de l'écosystème sur l'interface FFI de CPython, qui n'est pas spécifiée à ce niveau et qui est trop spécifique pour permettre des vraies différences d'implémentation. (Au contraire le travail sur la FFI du projet Pypy a vocation à être portable sur d'autres implémentations, il n'est pas Pypy-spécifique.)

                            La plupart de ces langages n'ont qu'une implémentation de référence.

                            Encore une fois, tout le monde est d'accord là-dessus. C'est pour ça que j'explique (dans mes messages ci-dessus) qu'il existe des langages vraiment multi-implémentations, mais que Haskell ne l'est plus aujourd'hui—Python ne l'a jamais été, mais ce n'est pas une critique,.

                            Notons que dans le cas de IronPython, comme c'est la VM de .Net qui est utilisée, l'implémentation n'est pas forcément mauvaise en termes de perf…

                            Ça c'est la théorie. En pratique les VMs pensées pour des langages statiques ont du mal à avoir de vraiment bonnes perfs pour des langages trop dynamiques comme Python ou Ruby (c'est pour ça que MS et Oracle investissent dans le Dynamic Language Runtime, Truffle etc., mais ça reste difficile d'être compétitif). En plus de ça, les programmes Python utilisés en pratique sont plombés par un grand nombre d'appel à C en utilisant une FFI spécifique à CPython et peu portable; dès que l'implémentation alternative, même si elle marche super bien sur du code "Python pur", a un coût de conversion important pour la FFI Cpython, les performances s'écroulent en pratique.

                            Par exemple, a-t-on des mesures de quelqu'un ayant récemment fait tourner Django sur IronPython ? Désolé de faire le casse-pieds, mais d'une part je ne suis même pas sûr que ça marche aujourd'hui (enfin s'il y a vraiment des utilisateurs de IronPython ça doit pouvoir se trouver), et d'autre part je m'attends à ce que les performances soient plutôt décevantes.

                            Ça n'enlève bien-sûr rien à la potentielle niche de gens voulant ajouter du scripting en Python à leur application .NET. Là les performances sont sans doute moins importantes, et la compatibilité avec les bibliothèques Python existantes non plus. Il n'empêche que je suis sceptique sur la viabilité du pojet à terme si c'est le seul public.

  • # Autres changements interessants

    Posté par  (site web personnel) . Évalué à 10.

    Je vais compléter ma propre dépêche de nouveaux trucs intéressants que je n'avais pas encore noté ou réalisé l’intérêt.

    Amélioration du Shell ghci.

    Trois petites améliorations simple du shell :

    • Les erreurs sont maintenant listées avec de beaux caractères unicode avancés pour faire les points d'une liste.
    • Il est maintenant possible de définir dans le shell des fonctions et des noms sans devoir utiliser let. Au lieu de taper let x = 5, on peut taper x = 5.
    • Dernier point que j'apprécie particulièrement. En haskell, une erreur de type est critique et fait planter la compilation. C'est énervant lors d'un refactoring car on ne peut pas lancer ses tests unitaires tant que tout n'est pas fixé. Heureusement GHC permet de compiler avec -fdefer-type-errors qui transforme les erreurs de type en erreur au runtime. Maintenant le shell permet de charger un module normalement avec :load ou en reportant les erreurs au runtime, avec :load!.

    Extensions XTypeApplications

    Cette extension permet de spécialiser lors de l'appel une fonction polymorphique.

    Prenons par exemple la paire de fonction encode et decode de Data.Serialize (package cereal) présentée rapidement dans la dépêche.

    > :type decode
    decode :: Serialize a => ByteString -> Either String a
    > :type encode
    encode :: Serialize a => a -> ByteString

    La fonction encode prend n'importe quel type a et retourne une ByteString, bref elle serialize. La fonction decode réalise l'opération inverse, prenant une ByteString et retournant un Either String a, c'est à dire soit une erreur sous forme de String, soit un type a.

    Le problème c'est que encoder est simple, le type a est connu puisque on le passe directement en paramètre, ici avec une liste de booléens [Bool] :

    > encode [True, False, True]
    "\NUL\NUL\NUL\NUL\NUL\NUL\NUL\ETX\SOH\NUL\SOH"

    Pour le décodage c'est plus complexe, puisque on ne connait pas le type de sortie :

    > :type decode (encode [True, False, True])
    decode (encode True) :: Serialize a => Either String a

    Très souvent le moteur d'inférence s'en sort car ce type va être utilisé directement après, par exemple :

    > (decode (encode [True, False, True])) == Right [True, False, True]
    True

    Ici on compare le contenu décodé à Right [True, False, True], ce qui veut dire qu'il a bien réussis à décoder. En écrivant cette comparaison, le type à décoder ne peut qu'être un Bool.

    Mais si on se sert après d'une fonction polymorphique pour lequel le type peut être ambigu, par exemple la fonction length qui accepte tout type de conteneur, alors il faut spécifier le type.

    Plusieurs solutions s'offrait à nous avant :

    • spécifier le type de la fonction decode. Rappel, celui-ci est ByteString -> Either String a et il faut forcer a, ce qui donne une annotation de type :: ByteString -> Either String [Bool]
    • spécifier le type de la valeur de sortie. Rappel, celui-ci est Either String a, il faut donc forcer a, ce qui donne :: Either String [Bool].

    Dans ce cas simple c'est supportable, mais dans le cas d'un type bien plus complexe, il est rébarbatif de recopier tous les types déjà connus (ici ByteString et Either String) pour ne specifier qu'un petit type polymorphique. GHC 8.0 permet de spécifier spetialement les types polymorphique d'une fonction gràce à @. Dans le cas de decode il n'y a qu'un seul type à spécifier et cela se fait donc de la façon suivante : decode @[Bool].

    Comparez les trois approches :

    (decode :: ByteString -> Either String [Bool]) (encode [True, False, True)
    (decode (encode [True, False, True])) :: Either String [Bool]
    decode @[Bool] (encode [True, False, True])

    Plus compact et plus clair.

  • # Point de vue de novice

    Posté par  (site web personnel) . Évalué à 4.

    Bonjour, je me suis mis à Haskell il y a quelques semaines. C'est un langage particulièrement difficile à apprendre, surtout si on vient d'un monde impératif, paradigme qui constitue celui de la majorité des langages en production au 21ème siècle.

    En particulier faire des IO est un peu délicat lors d'un premier jet. En fait, pour être productif, il faut y passer beaucoup de temps, un apprentissage sérieux prendra plus de six mois.

    Je trouve l'exemple donné ci-dessus un peu élaboré, et il manque les déclarations de type. Celles ci ne sont pas obligatoires en Haskell mais vivement conseillées car elles permettent de mieux comprendre le code et évitent aussi les erreurs lors de la rédaction d'un programme.

    Voici quelques exemples de fonctions élémentaires réalisées avec Haskell :

    {- Prends un argument qui supporte '*' et retourne le double -}
    dbl x = x*2
    
    {- double chaque élément dans une liste (list comprehension) -}
    dbll lx = [ dbl k | k<- lx ]
    
    {- renvoie la liste des digits dans une base donnee (attention dans l'ordre inverse) -}
    get_digits_basis n b = if n<=0 then [] else m:(get_digits_basis (quot (n - m) b) b) where m = mod n b
    
    {- Même chose mais avec des guards -- plus lisible -- et avec une déclaration de types -}
    get_digits_basis2 :: Int -> Int -> [Int]
    get_digits_basis2 n b
        | n <= 0     = [] 
        | otherwise = m:(get_digits_basis2 (quot (n - m) b) b) 
        where m = mod n b
    
    {- application en base 10 -}
    get_decimal n = reverse(get_digits_basis2 n 10)
    
    {- application en base 2  -}
    get_binary n = reverse(get_digits_basis2 n 2)
    
    {- renvoie la somme des digits d'un nombre entier quelconque -}
    sumDigits [] = 0
    sumDigits (x:y) = sum (get_decimal x) + sumDigits y

    Être productif en Haskell demandera sans doute de longs mois d'apprentissage, en tous cas je n'en suis pas au bout.

    • [^] # Re: Point de vue de novice

      Posté par  (site web personnel) . Évalué à 8.

      il faut y passer beaucoup de temps, un apprentissage sérieux prendra plus de six mois.

      Un outil complexe demande forcement un apprentissage pour maitriser, mais tu peux commencer à écrire du code qui marche assez rapidement. J'ai écris un lancer de rayon dans ma première semaine d'Haskell, alors que je n'avais encore rien compris au langage (Et j'ai du tout réécrire après ;)

      il manque les déclarations de type. Celles ci ne sont pas obligatoires en Haskell mais vivement conseillées car elles permettent de mieux comprendre le code et évitent aussi les erreurs lors de la rédaction d'un programme.

      L'absence d'annotation de type est un choix de ma part pour montrer que le coté typage statique du langage n'impose pas forcement une syntaxe verbeuse. Cet exemple permet de montrer un "vrai" problème qui mixe des entrées-sorties, des structures de donnée autre que la liste et de la composition de fonctions. Internet est plein d'introduction à Haskell à base de Fibonnaci, de quick-sort (bugés) et de factorielle, mais les exemples montrent rarement d'entrées-sorties ce qui laisse sur cette fausse impression que c'est compliqué.

    • [^] # Re: Point de vue de novice

      Posté par  . Évalué à 1.

      Dans les cours/tuto on montre souvent des codes comme ça, mais il avec le temps, tu en écriras de moins en moins aussi

      Pour ton exemple, on peut s'en sortir avec Data.Digits, sum, fold, map
      Ces deux dernières sont vraiment des outils auxquels on s'habitue.

      Bon courage pour la suite en tout cas ;)

  • # Sympa la référence à docteur Folamour

    Posté par  . Évalué à 3.

    "Comment j'ai appris à ne plus m'inquiéter et à aimer les types"
    Sachant que l'original est :
    "comment j'ai appris à ne plus m'en faire et à aimer la bombe"
    Cela m'a donné envie de revoir le film ;-)

  • # Unicode et Haskell

    Posté par  . Évalué à 3.

    Rassurez vous, Haskell gère très bien l'Unicode, mais pas pour l'affichage dans une structure imbriquée.

    Ce passage peut intriguer… Le problème n'est pas tant qu'Haskell ne gère pas l'affichage d'Unicode dans une structure imbriquée (pour le coup ce serait problématique !) mais qu'ici Guillaum utilise la fonction print, cette fonction affiche le résultat de show sur son argument et cette fonction est destiné au débogage ou à l'affichage rapide de valeurs quelconques dont le type est une instance de la typeclass Show. L'objectif n'a jamais été de présenter correctement ou joliment une structure de donnée mais plutôt d'en donner une expression qu'on puisse éventuellement recoller dans son code sans problème. En particulier l'instance de Char est telle que lorsqu'on show un caractère hors de l'ASCII, son code caractère est affiché à la place, de sorte qu'on puisse le recoller dans le source sans aucun problème de compatibilité.

    Si à la place vous utilisez putStr sur une String comportant de l'Unicode, vous verrez que cela fonctionne parfaitement (pour peu que votre locale soit bien réglée).

    Ici par exemple on pourrait utiliser :

    showResult :: Maybe [String] -> IO ()
    showResult m = putStr (maybe "Pas dans le dictionnaire" unlines m)

    Qui afficherait un anagramme (accents compris) par ligne, ou "Pas dans le dictionnaire" si le mot n'est pas trouvé.

    Ligne 11, définition de getAnagram qui regarde dans la structure si la clé y est. Pas d'opérateur pour l'accès aux cases d'un dictionnaire en Haskell, il faut utiliser Map.lookup qui renvoie Nothing si la clé n'existe pas ou Just value si elle existe.

    Il existe en fait un opérateur : (!) comme dans dict ! "clé" mais la plupart des utilisateurs d'Haskell préfère la fonction lookup car celle-ci renvoie une valeur de type Maybe ... et oblige donc l'utilisateur à traiter le cas où la clé n'existe pas dans le dictionnaire plutôt que de soulever une exception comme le fait (!). On peut dire que cette différence résume bien l'esprit d'Haskell : ne pas utiliser de fonction partielles (qui peuvent échouer sur certaines entrées), préférer encoder le fait qu'une fonction puisse échouer dans son type de sorte qu'un utilisateur doive traiter le cas d'erreur (mais fournir des combinateurs comme maybe pour rendre cela facile !!).

    Notez que les opérateurs en Haskell sont des fonctions comme les autres (en dehors de leur syntaxe) et peuvent être définis dans n'importe quel module. C'est une force qui peut parfois être légèrement abusée au goût de certains… ;-)

    • [^] # Re: Unicode et Haskell

      Posté par  (site web personnel) . Évalué à 3.

      Merci pour ces précisions. J’avoue que je me suis permis ces raccourcis car la dépêche (initialement un journal écrit entre midi et deux pendant un moment de procrastination) était déjà fort long. Comme je le disais c'est un exemple perfectible que je ne voulais pas surcharger ;)

      Notez que les opérateurs en Haskell sont des fonctions comme les autres (en dehors de leur syntaxe) et peuvent être définis dans n'importe quel module. C'est une force qui peut parfois être légèrement abusée au goût de certains… ;-)

      (lens mon amour…)

      Je me permet d'ajouter, pour tout ceux qui seraient tentés par Haskell, au début vous aller croiser quelques opérateurs dont quatre en particulier : le couple <$> et <*> ainsi que le couple >> et >>=. Ces opérateurs font initialement peur, mais il peuvent se comprendre sur des cas très simples et après s'appliquer dans de nombreux cas génériques et deviennent aussi naturel que le ; et le for peut l'être en C.

      • [^] # Re: Unicode et Haskell

        Posté par  . Évalué à 3.

        (lens mon amour…)
        […]Ces opérateurs font initialement peur, […]

        D'un côté les adeptes de la programmation fonctionnelle pratiquent une drôle de cuisine : des bananes aux lentilles enveloppées dans des fils barbelés. Ça fait un peu gloubiboulga à la mode Casimir, ce n'est pas très appétissant ! :-P

        Alors qu'au fond, même ma nièce de huit ans a compris le principe des anamorphismes (lens), catamorphismes (banana) et hylomorphismes (la composition des deux premiers). J'en veux pour preuve son cahier d'exercices sur les multiplications dont voici un extrait :

          142
        *   4
        -----
            8 <-   2 * 4
        + 160 <-  40 * 4
        + 400 <- 100 * 4
        -----
        = 568
        

        Pour appliquer l'algorithme de multiplication que lui a enseigné son institutrice, elle commence par prendre l'entier 142 qu'elle transforme en une liste d'entiers : [2; 4; 1] (lentilles et anamorphismes, vous voilà !). Ensuite, elle applique la multiplication sur chacun des éléments de la liste (soit un map qui est un cas de catamorphismes ou bananas) à l'aide d'un tableau associatif, autrement connu sous le nom de tables de multiplication, pour obtenir la liste [8; 160; 400] . Puis, pour terminer, elle ajoute le tout (ce qui est toujours un banana) pour obtenir son résultat: 568. \o/

        Bilan des courses : un anamorphisme puis deux catamorphismes pour obtenir un hylomorphisme, plus connu sous le nom de multiplication. :-P

        Pour la notion de monade et de binding entre monades (>>=) c'est aussi simple : une monade est un monoïde dans la catégorie des endofoncteurs ! Cette formulation paraît absconse ? Pour les lecteurs plus familiers avec le paradigme de la programmation orientée objet, la lecture de l'article Dieu a-t-il programmé le monde en Java ? sur le blog de la Société Informatique de France leur permettra sans doute d'y voir plus clair. D'autant que je trouve que le concept de monade en programmation fonctionnelle, qui a été nommé ainsi en théorie des catégories en référence à la monadologie leibnizienne, capture mieux la signification qu'avait ce terme chez Leibniz que le concept d'encapsulation en POO. Et puis, on peut aussi faire des monades en C++.

        C'était mon message a teneur humoristique du samedi midi. Je milite également pour que l'on soit plus rigoureux dans l'expression de certaine proposition, et que l'on ne dise plus : « la terre est ronde », mais plutôt : « d'un certain point vue, une équipotentielle du champ de gravitation terrestre est homéomorphe à la sphère S_2 ». :-D

        Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

        • [^] # Re: Unicode et Haskell

          Posté par  . Évalué à 1.

          Pourrais-tu développer l'analogie « anamorphosisme = lens » ? Parce que d'un côté je n'ai pas de soucis avec tout ce qui est anamorphosismes/catamorphismes/hylomorphismes, mais la librairie lens en elle même semble avoir un peu plus que simplement ces concepts (ou alors ils sont cachés derrière des types atroces). J'avais un jour cru comprendre que c'était lié (de très loin) à la notion de co-monade … mais je ne retrouve pas le lien.

          Dans la même veine, les enfants ne savent pas compter, par contre ils peuvent construire des bijections et des injections très tôt pour détecter des différences de cardinalité entre deux ensembles. L'exemple évident est « compter sur ses doigts », qui est précisément construire une injection de l'ensemble « doigts » vers l'ensemble des objets que l'on veut dénombrer. En revanche, je ne suis pas certain qu'il soit pédagogique d'expliquer le concept général de bijection avant de savoir compter, tout comme les hylomorphismes peuvent rester des objets obscurs sans que cela ait un impact trop important sur le développement intellectuel :-p.

          Les monades en informatique peuvent posséder un état interne, certes, mais elles ne sont pas vues comme des « entités ». Ne serait-ce que parce qu'on a une fonction T (T a) -> T a, qui permet précisément de prendre une chose qui est une entité composée et de l'aplatir … alors que (de ce que j'ai compris de la monadologie) les entités devraient être « indépendantes » et « autonomes » non ?

          Si je devais donner un truc qui se rapproche plus de cette idée que les objets, je parlerais de processus légers dans une application, par exemple en Erlang, où il y a exactement les caractéristiques d'individualité, d'opacité, d'autonomie, et d'échange de message. Par contre, il manque la possibilité d'introspection, mais je ne sais pas dans quelle mesure c'est possible en Erlang, ou demandé pour une monade.

          Quand on raisonne à homéomorphisme près c'est pas super cool non ? Tu peux au moins demander que ce soit un Ck-difféormorphisme (avec k grand, par exemple 200000) pour éviter la possibilité d'avoir des trucs trop irréguliers …

          • [^] # Re: Unicode et Haskell

            Posté par  . Évalué à 1.

            Pourrais-tu développer l'analogie « anamorphosisme = lens » ?

            C'est dans le contenu du premier lien de mon message (la cuisine gloubiboulga ;-) où le titre original de l'article est : Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire. Les anamoprhismes sont des générateurs de structures (le unfold dont l'opérateur contraire fold est un catamorphisme). Compter sur ces doigts, pour reprendre un de tes exemples, voilà le premier anamorphisme que rencontre un enfant ! ;-) C'est un générateur de nombre entier sous leur forme unaire ou entier de Peano (type peano = Zero | Succ of peano en OCaml) qui ressemble fortement au type des listes (on peut les voir comme une liste dont la valeur des éléments est constante Nil | Cons of unit * peano de type unit list, et le morphisme naturel entre les deux types calcule la longueur de la liste).

            Ces trois types d'opérations sont fécondes. Dans ce journal, par exemple, l'auteur utilise les template C++ 14 pour implémenter un interpréteur d'algèbre linéaire sur des vecteurs, et plonger le langage cible dans le langage d'origine. Il utilise le pattern des visiteurs, là où en programmation fonctionnelle on passerait par un catamoprhisme (l'exemple est en F#). Mais pour obtenir le même fonctionnement que lui, ne pas allouer d'objets intermédiaires, on chercherait à produire l'hylomorphisme correspondant à la composition de l'anamorphisme qui réifie l'arbre d'évaluation et au catamorphisme qui le consomme :

            A recursive function h : 'a -> 'b whose call-tree is isomorphic to a cons-list, i.e., a linear
            recursive function, is called a hylomorphism. […]

            A hylomorphism corresponds to the composition of an anamorphism that builds the call-tree as
            an explicit data structure and a catamorphism that reduces this data object into the required
            value.

            Functional Programming with Bananas, Lenses, Envelopes and Barbed Wire, p. 4-5

            On passerait directement par la pile d'appel sans allouer d'intermédiaire. L'idée est assez proche de ce commentaire de gasche sur stackoverflow, reprise ici pour une version en Haskell.

            En revanche, je ne suis pas certain qu'il soit pédagogique d'expliquer le concept général de bijection avant de savoir compter, tout comme les hylomorphismes peuvent rester des objets obscurs sans que cela ait un impact trop important sur le développement intellectuel :-p.

            Nicolas Bourbaki s'inscrirait en faux contre de tels propos. Il a grandement contribué à la réforme de l'enseignement des mathématiques sous la forme des mathématiques modernes. Cette réforme fut sur le plan pédagogie, et cela de manière surprenante, un véritable échec ! :-P

            Tu auras bien compris que mon propos empruntait la démarche inverse, en s'adressant à des adultes qui ont déjà une certaine maîtrise de l'abstraction pour leur montrer que ces concepts abstraits, au premier abord déroutant, ils les manipulaient depuis l'enfance sans les avoir nommé ainsi. ;-)

            Pour la monadologie cela se discute. Cela étant, Saunders Mac Lane leur a donné ce nom en référence au concept de Leibniz.

            Quand on raisonne à homéomorphisme près c'est pas super cool non ? Tu peux au moins demander que ce soit un Ck-difféormorphisme (avec k grand, par exemple 200000) pour éviter la possibilité d'avoir des trucs trop irréguliers …

            Là-dessus, je partage le point de vue de Poincaré (qu'il a exposé dans La valeur de la Science, ou La Science et l'hypothèse, je ne sais plus lequel des deux) selon lequel ce n'est pas spécialement pertinent, en physique théorique, de distinguer continue et différentiable. Mais peu importe, l'essentiel est de faire référence à l'identité topologique avec la sphère S_2. Je vais fonder le comité des adeptes de la capillotétratomie et de la diptérosodomie, et rejoindre le groupe des xyloglottes. :-D

            Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

            • [^] # Re: Unicode et Haskell

              Posté par  . Évalué à 2.

              Nicolas Bourbaki s'inscrirait en faux contre de tels propos. Il a grandement contribué à la réforme de l'enseignement des mathématiques sous la forme des mathématiques modernes. Cette réforme fut sur le plan pédagogie, et cela de manière surprenante, un véritable échec ! :-P

              Soit dit en passant, vu qu'en fac on « réapprend » les maths apprises depuis la primaire, je pense que l'approche de « Bourbaki » se défend parfaitement en premier cycle universitaire.

              • [^] # Re: Unicode et Haskell

                Posté par  . Évalué à 1.

                À ce niveau, cela se tient et il me semble que c'est toujours ainsi que cela se pratique (lorsque j'étais étudiant puis chargé de TD c'est ainsi que l'on faisait). Lors de mon premier cours de sup', notre professeur nous avait dit une chose du genre : « pour la première fois de votre vie, vous allez faire des mathématiques » (c'est volontairement provocateur, mais il y a du vrai dedans : cela traduit surtout un changement complet de méthode dans le traitement de cette science).

                Cela étant, commencer cette approche dès le primaire et le secondaire ne pouvait qu'être voué à l'échec. Il ne faut pas avoir soi même d'enfant, ou avoir côtoyé des enfants pour imaginer qu'une telle méthode puisse fonctionner.

                Les actes logiques de l'entendement qui produisent les concepts selon la forme sont :

                • la comparaison c'est-à-dire la confrontation des représentations entre elles en relation avec l'unité de la conscience ;
                • la réflexion c'est-à-dire la prise en considération de la manière dont diverses représentations peuvent être saisies dans une conscience ;
                • enfin l'abstraction ou la séparation de tout ce en quoi pour le reste les représentations données se distinguent.

                Remarques. 1) Pour faire des concepts à partir de représentations, il faut donc comparer, réfléchir et abstraire, car ces trois opérations logiques de l'entendement sont les conditions générales et essentielles de production de tout concept en général. — Par exemple, je vois un pin, un saule et un tilleul. En comparant tout d'abord ces objets entre eux, je remarque qu'ils diffèrent les uns des autres au point de vue du tronc, des branches, des feuilles, etc…; mais si ensuite je réfléchis uniquement à ce qu'ils ont de commun entre eux, le tronc, les branches et les feuilles-mêmes et si je fais abstraction de leur taille, de leur taille, etc… j'obtiens un concept d'arbre.

                2) On n'emploie pas toujours correctement en logique le terme : abstraction. Nous ne devons pas dire : abstraire quelque chose (abstrahere aliquid), mais abstraire de quelque chose (abstrahere ab aliquo). Si par exemple dans un drap écarlate je pense uniquement au rouge, je fais abstraction du drap; si je fais en outre abstraction de ce dernier en mettant à penser l'écarlate comme une substance matérielle en général, je fais abstraction d'encore plus de déterminations, et mon concept est devenu par là encore plus abstrait. Car plus on écarte d'un concept de caractères distinctifs des choses, c'est-à-dire plus on en abstrait de déterminations, plus le concept est abstrait. C'est donc abstrayants (conceptus abstrahentes) qu'on devrait nommer les concepts abstraits, c'est-à-dire ceux dans lesquels d'avantage d'abstractions ont eu lieu. Ainsi par exemple, le concept de corps n'est pas à proprement parler un concept abstrait; car du corps lui-même je ne puis faire abstraction puisque dans ce cas je n'en aurais pas le concept. Mais il faut bien que je fasse abstraction de la taille, de la couleur, de la dureté ou de la fluidité, bref de tous les déterminations spéciales des corps particuliers — Le concept le plus abstrait est celui qui n'a rien de commun avec ce qui diffèrent de lui. C'est le concept de quelque chose; car le concept qui s'en distingue est celui de rien et il n'a donc rien de commun avec le quelque chose.

                Kant, Logique.

                Pour que le processus d'abstraction aboutisse, et lorsque l'on s'adresse à quelqu'un pour qu'il comprenne où l'on veut en venir, il faut déjà pouvoir comparer. Et cette comparaison, suivie de la réflexion, nécessite une grande familiarité avec les objets à partir desquels l'on cherche à produire un concept plus général les englobant tous par abstraction. Parler d'arbre à une personne qui n'en a jamais vu un seul, ni plusieurs différents pour les comparer, cela ne peut aboutir qu'à parler dans le vide et à de l'incompréhension.

                Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                • [^] # Re: Unicode et Haskell

                  Posté par  . Évalué à 2.

                  Cela étant, commencer cette approche dès le primaire et le secondaire ne pouvait qu'être voué à l'échec. Il ne faut pas avoir soi même d'enfant, ou avoir côtoyé des enfants pour imaginer qu'une telle méthode puisse fonctionner.

                  Je ne pense pas qu'on puisse dire cela puisqu'il y a des enfants qui ont, au contraire, beaucoup apprécié les maths modernes, d'une façon qui transcende la reproduction sociale—certains de ces enfants sont membres de la communauté scientifique aujourd'hui.

                  • [^] # Re: Unicode et Haskell

                    Posté par  . Évalué à 2. Dernière modification le 14 juin 2016 à 21:18.

                    Cela ne doit tout de même pas représenter une grande proportion de cette génération. Si l'on suit le principe de Poincaré selon lequel « on ne devient pas mathématicien, on naît mathématicien », ils devaient être de cela. Pour les autres, j'ai toujours entendu dire que ce fût un calvaire. N'ayant pas vécu moi même cette époque, je ne peux que me baser sur des ouïe dires, mais de ce que l'on m'en a dit je ne pense pas que cela m'aurait dérangé non plus comme approche.

                    Néanmoins, lorsque j'étais étudiant je faisais du soutien scolaire pour des collégiens et des lycéens afin de financer mes études, et pour nombre d'entre eux l'abstraction était une peine. Pour prendre un exemple, j'ai eu plus d'un élève de troisième qui comprenait difficilement que l'on puisse mettre x à la place de prix du kilo de bananes dans un système d'équations, et ils préféraient recopier l'expression complète à chaque ligne de calcul. Après, mon expérience m'a apprise que je ne suis pas un grand pédagogue, même si je m'en sors mieux sur des cours particuliers que des cours de groupes.

                    Sapere aude ! Aie le courage de te servir de ton propre entendement. Voilà la devise des Lumières.

                    • [^] # Re: Unicode et Haskell

                      Posté par  . Évalué à 4.

                      J'ai des gens de ma famille qui ont eu à subir les maths modernes. Ça les a franchement traumatisés. Je tends à penser comme toi : ceux qui ont une affinité naturelle avec certains raisonnements faits en maths ont été encouragés, mais ce n'était sans doute pas la majorité (même si je suis le premier à dire que mes exemples « perso » tiennent bien entendu de l'anecdote).

                      Après, je trouve qu'en général la France a une approche des maths parfois trop formelle, mais au moins (même au collège ou au lycée, et très clairement dans le supérieur), elle est relativement logique. Aux USA par exemple, l'apprentissage de l'algèbre relationnelle est rarement « constructiviste » : on apprend à se servir d'un déterminant ou d'une matrice avant de savoir ce qu'est un espace vectoriel. Ce n'est qu'un exemple parmi tant d'autres, mais c'est le côté trop pragmatique des choses : on veut que les étudiants soient capables d'utiliser matlab/octave très vite pour faire des trucs en génie électrique/mécanique/chimique, et on verra plus tard pour expliquer pourquoi ça marche.

                      Ça donne des trucs rigolos, du genre des bouquins qui s'intitulent « Linear Algebra Done Right » et qui … suivent en gros une façon logique et constructiviste pour enseigner l'A.L.

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.