Sortie du Glorious Haskell Compiler 7.8

Posté par  . Édité par Piezo, BAud, Bruno Michel, Benoît Sibaud, tuiu pol, Lucas, ZeroHeure et claudex. Modéré par tuiu pol. Licence CC By‑SA.
Étiquettes :
52
13
avr.
2014
Programmation fonctionnelle

Ghc, le Glorious Haskell Compiler est sorti le 9 avril 2014 en version 7.8.1. Il s'agit du compilateur le plus populaire pour le langage Haskell. Haskell est un langage de programmation purement fonctionnel, avec un système de types sophistiqué. C'est le principal langage généraliste à utiliser une stratégie d'évaluation non-stricte par défaut : la valeur d'une expression n'est calculée que si elle est nécessaire.

Sommaire

Haskell en trois mots

Haskell est un langage de programmation purement fonctionnel, avec un typage statique fort, et une stratégie d'évaluation non stricte.

Le Glasgow Haskell Compiler est le principal compilateur Haskell, bien qu'il en existe d'autres.

Quelques exemples

L'inévitable hello world :

main = putStrLn "Saluton Ĉirkaŭaĵojn!"

Suite de Fibonnacci :

fibs :: Int -> Int
fibs 0 = 0
fibs 1 = 1
fibs n = fibs (n-1) + fibs (n-2)

-- Sous forme de liste infinie
fibs':: [Int]
fibs' = 0:1:(zipWith (+) fibs' (tail fibs'))

Un langage fonctionnel pur

La spécificité la plus marquante de Haskell, c'est qu'il s'agit d'un langage fonctionnel pur : une fonction appelée avec les mêmes arguments renverra toujours le même résultat, contrairement à une fonction comme input(message) en python.

Cela rend possible la transparence référentielle : une expression peut être remplacée par son résultat sans changer le comportement de l'application. Le raisonnement sur les équations est mené par substitution sans inquiétude des effets de bord.

Les entrée-sorties et autres effets de bord sont bien sûr possibles, mais ils sont distingués dans le système de type des calculs sans effets de bord. Cela permet des optimisations assez poussées et garanties correctes sur ces derniers. Cette distinction des effets de bord dans le système de type est déroutante au début, mais avec l'expérience, elle apporte un réel confort.

Avec un typage puissant

Le typage statique permet de vérifier à la compilation la cohérence d'un programme et de détecter de nombreuses classes d'erreurs fréquentes. Un type statique est une approximation réalisée pendant la compilation des valeurs rencontrées au cours de l'exécution. Il est extrêmement difficile de concevoir un système de type à la fois flexible (faiblement verbeux, avec une inférence utile), complet (de grain fin, éliminant plus de bugs) et correct (pas de programmes justes rejetés). Haskell essaye de trouver le juste milieu pour que la programmation généraliste soit efficace.

Les aficionados du typage dynamique et du « duck typing » argumentent que, lors du développement avec les langages statiquement typés (typiquement C/C++), une grande partie des ressources sont gaspillées à combattre le système de type. Le typage statique de Haskell est rendu extrêmement flexible grâce au polymorphisme paramétrique, similaire aux templates en C++, mais avec des possibilités d'inférence bien plus développées.

Un type polymorphique peut être restreint à l'aide de contraintes, c'est-à-dire un ensemble de spécifications (ou typeclasses). Par exemple pour insérer un élément dans un Set, il est nécessaire de pouvoir le comparer aux éléments déjà présents avec la méthode compare de la typeclasse Ord, d'où la contrainte Ord a => dans le type de insert :

insert :: Ord a => a -> Set a -> Set a

En pratique, dans les cas courants, la seule différence induite par le système de types entre un code haskell et le code python correspondant est la présence (facultative mais habituelle) en haskell de la signature de chaque fonction… Signatures qui sont en cours d'adoption par la communauté python.

Les nouveautés de ghc 7.8.1

Changements du langage

Typed holes

Grâce au typage fort de haskell, on se trouve souvent à utiliser les types pour guider la conception : on écrit d'abord les types dont on va avoir besoin, puis les signatures des fonctions. Ensuite, de proche en proche, l'implémentation des fonctions est guidée par les signatures. Bien sûr, dans les cas les plus complexes, il faut d'autres ressources que cette méthode, mais on y devient rapidement accro. L'extension typed holes (littéralement « trous typés ») permet d'utiliser cette démarche dans le corps d'une fonction.

Cette extension est souvent utilisée conjointement avec le drapeau -fdefer-type-errors qui transforme les erreurs de typage en warnings. Dans ghci, cela permet de charger toutes les définitions et de les manipuler même si leurs types sont incorrects. Une expression mal typée ou contenant des typed holes est équivalant à undefined et déclenchera une erreur uniquement si elle est évaluée.

En pratique, un hole est un _ placé dans le code à la place d'une expression manquante (ici à la dernière ligne):

getSensorReading :: IO Double
getSensorReading = return 123.45
 -- TODO: utiliser vraiment la sonde thermique

main = do
    tKelvins <- getSensorReading
    putStrLn ("Aujourd'hui il fait " ++ _ ( tKelvins - 273.15) ++ " degrés")

À la compilation, ghc émet une erreur (ou un warning si -fdefer-type-errors est activé) pour chaque hole, et affiche son type:

    Found hole ‘_’ with type: Double -> [Char]
    Relevant bindings include
      tKelvins :: Double (bound at toto.hs:6:5)
      main :: IO () (bound at toto.hs:5:1)
    In the expression: _
    In the first argument of ‘(++)’, namely ‘_ (tKelvins - 273.15)’
    In the second argument of ‘(++)’, namely
      ‘_ (tKelvins - 273.15) ++ " degrés"’

Dans notre exemple, le warning à la compilation nous indique que pour afficher la température dans le putStrLn, il nous faudra une fonction de type Double -> String. Une simple recherche dans hoogle nous indique que la fonction en question n'est autre que show.

OverloadedLists

Haskell repose sur une syntaxe très concise, la majeure partie du langage (Opérateurs, type de base, etc) étant définie dans le Prelude.
En revanche, ce n'est pas le cas des expressions littérales comme les nombres, les chaines de caractères et les listes. Par exemple [1, 2, 3] est interprété par le compilateur comme 1:2:3:[], où (:) et [] sont les constructeurs du type liste défini dans le Prelude. OverloadedStrings autorisait déjà les chaînes de caractères littérales avec d'autres types que [Char], notamment le type Text pour l'encodage UTF-8.

Cette extension OverloadedLists est documentée, elle permet d'utiliser la syntaxe des littéraux listes avec d'autres types, comme par exemple les vecteurs, les chaînes du module Text ou encore les ensembles.

Pour utiliser cette extension, il faut disposer d'une instance de la classe IsList. Il faudra donc attendre que ces instances soient intégrées aux bibliothèques haskell pour vraiment en profiter.

PatternSynonyms

Une autre extension, PatternSynonyms permet de donner un nom à un motif pour le filtrage.

La puissance des types de donnée algébrique (ADT) repose sur la possibilité de filtrer les données par motif, comme en Scala :

type Errno = Int
data Error = ErrorLibC Errno
           | ErrorIO IOException

errorToStr :: Error -> String
errorToStr (ErrorIO ioe)     = "Haskell's IOException: " ++ show ioe
errorToStr (ErrorLibC errno) = "LibC's errno: " ++ show errno

Chaque constructeur du type Error défini un motif, utilisé par la fonction errorToStr pour filtrer les erreurs. En revanche il n'était pas possible de définir ses propres motifs en plus des constructeurs. PatternSynonyms permet maintenant cela, par exemple pour avoir plusieurs motifs par constructeurs :

pattern ErrorPERM  = ErrorLibC 1
pattern ErrorNOENT = ErrorLibC 2
pattern ErrorSRCH  = ErrorLibC 3

errorToStr :: Error -> String
errorToStr (ErrorIO ioe) = "Haskell's IOException: " ++ show ioe
errorToStr  ErrorPERM    = "Operation not permitted"
errorToStr  ErrorNOENT   = "No such file or directory"
errorToStr  ErrorSRCH    = "No such process"
errorToStr (ErrorLibC errno) = "LibC's unknown errno: " ++ show errno

Les motifs synonymes sont aussi utilisés pour cibler des données profondément imbriquées : pattern Greetings name = 'h':'e':'l':'l':'o':' ':name. Par défaut il sont bidirectionnels : Greetings "world" est une valeur. Il est possible de définir des synonymes unidirectionnels lorsqu'il n'existe pas de bijection.

TypeFamilies fermées

Il est possible de déclarer une famille de type et ses instances dans la même clause, par exemple:

type family TF a where
    TF ([Bool]) = Int
    TF ([Bool],[Bool]) = Double

La famille est alors fermée, c'est-à-dire qu'il n'est pas possible de déclarer de nouvelles instances de TF.

Améliorations du compilateur

De nombreuses améliorations portent sur le compilateur lui-même, sans modifier le langage. Il est toujours plus rapide et économe en mémoire, notamment pour l'inférence de types. La compilation en parallèle de modules est activable avec la nouvelle option -jN.

Sur Linux, FreeBSD et Mac OS X, GHCi utilise maintenant le chargeur de code dynamique du système. Auparavant, un chargeur maison liait dynamiquement du code statique (.a) en mémoire. Pour utiliser cette fonctionnalité, il est nécessaire de compiler avec -dynamic-too pour générer des bibliothèques dynamiques (.dyn.so).

Améliorations de la génération de code

La qualité du code que ghc produit s'améliore aussi :

  • ghc est capable d'utiliser plus de types «nus» (unboxed), avec l'activation de -funbox-small-strict-fields par défaut;
  • le gestionnaire d'entrée sortie se parallélise mieux, «de façon linéaire jusqu'à 32 cœurs»;
  • le nouveau générateur de représentation intermédiaire Cmm a été activé, après une gestation de plusieurs années;
  • de nouvelles primitives (PrimOps) pour les instructions SIMD sont disponibles, pour l'instant uniquement avec le générateur LLVM.

Amélioration du gestionnaire d'entrée-sorties

Le gestionnaire d'entrée-sorties était un des points les plus critiques de la performance du runtime haskell. Dans la version 7.8.1, il a été grandement amélioré, en particulier dans un contexte multi-processeurs. Haskell devient ainsi de plus en plus crédible comme meilleur langage impératif du monde.

Compilation vers iOS

La gestion de la compilation croisée a été améliorée, ce qui permet notamment de compiler vers les systèmes iOS.

Compilation vers javascript avec ghcjs

La sortie de la version 7.8 devrait être accompagnée de la première version publique de ghcjs, un compilateur haskell vers javascript. Ce langage devient de plus en plus l'assembleur du client web et ghc prend ainsi en charge cette plateforme. Par rapport à Fay, qui est plus ancien, ghcjs cherche à compiler tout le langage haskell et pas seulement un sous-ensemble.

L'introduction à ghcjs permet de se faire une bonne idée de la façon de l'utiliser, en particulier pour les parties spécifiques à javascript.

Vers ghc 7.10

La version 7.10.1 sera la prochaine version majeure de ghc. On trouve sur le trac de ghc une liste de choses qui devraient figurer dans cette prochaine version. Certaines nouveautés de cette version sont préparées dès la 7.8.

Monad / Applicative

Ces deux concepts permettent d'associer un effet à une valeur : IO Int peut par exemple représenter l'action de la lecture d'un entier présent dans un fichier. La notion de foncteur applicatif est plus générale : toutes les monades sont des foncteurs applicatifs, tandis que l'inverse n'est pas nécessairement vrai. L'exécution des effets d'une monade peut dépendre des valeurs obtenues des effets précédents, contrairement aux foncteurs applicatifs où les effets sont exécutés dans l'ordre des arguments.

La classe de type Monad fut introduite dans les premières versions du langage pour gérer les entrées/sorties avec la monade IO. Lorsque, plus tard, la classe de type Applicative apparût dans la bibliothèque de base, Monad n'a pas été modifiée pour avoir Applicative comme super-classe.

Le but de la proposition Functor < Applicative < Monad est de corriger ce problème pour réduire la redondance (par exemple entre return et pure, liftM et fmap).
Ce changement, susceptible de rendre du code incompatible, est déjà signalé par un warning dans GHC 7.8 et sera officialisé dans la version 7.10.

Autres actualités de haskell

Olivier Charles a publié sur son blog l'édition 2013 de sa chronique 24 Days of Hackage qui offre une série d'introductions (avec des exemples) aux paquets majeurs disponibles sur Hackage. L'édition 2012 vaut également le détour, car elle couvre des paquets encore plus incontournables (base, containers, transformers, etc.).

Hackage 2

Le site hackage qui répertorie les paquets haskell libres, exécutables et bibliothèques est passé en version 2 en septembre 2013. L'aspect visuel a été amélioré, la recherche concerne maintenant aussi la description des paquets. La plupart des informations sont plus accessibles, en consultation mais aussi à la modification : le nouveau hackage est plus permissif, et il a une API rest plus étendue.

D'autres fonctionnalités ont été développées mais pas encore déployées, comme la gestion flexible des tags et le calcul des dépendances inverses.

Haskell platform

Il n'est généralement pas recommandé d'installer ghc seul, mais plutôt de récupérer la plateforme haskell, qui contient ghc ainsi que les bibliothèques les plus essentielles du monde haskell. Comme la version 7.8 de ghc a tardé, la plateforme n'a pas été mise à jour depuis la version 2013.2 de mai 2013. Une nouvelle version, adaptée à ghc 7.8 devrait sortir prochainement.

Aller plus loin

  • # Typos et signatures en python

    Posté par  . Évalué à 1.

    Dans ghci, cela permet charger toutes les définitions

    Dans ghci, cela permet de charger toutes les définitions

    ce n'est pas le cas expressions littérales comme les nombres

    ce n'est pas le cas des expressions littérales comme les nombres

    Chaque constructeurs du type Error

    Faut-il un 's' ici ?

    La famille est alors fermée, c'est à dire

    La famille est alors fermée, c'est-à-dire

    Monad n'a pas modifiée

    Monad n'a pas été modifiée

    à la modification:

    à la modification_:

    Signatures qui sont en cours d'adoption par la communauté python.

    Fais-tu référence à Cython ? (sinon ça m'intéresse de savoir plus précisément !)

    Merci pour cet article, c'est très intéressant !

    • [^] # Re: Typos et signatures en python

      Posté par  . Évalué à 2.

      Je suppose qu'il fait référence à http://legacy.python.org/dev/peps/pep-0362/ pour l'histoire des signatures en python. Merci également pour cette article effectivement très intéressant !

      • [^] # Re: Typos et signatures en python

        Posté par  . Évalué à 1.

        D'accord merci, mais j'ai du mal à voir le lien entre les signatures Haskell et ce qui est proposé dans la PEP 362. Il ne me semble pas avoir vu la possibilité d'annoter les signatures avec les types de chaque argument (ce qui ne serait pas forcément très pythonic si ?). J'ai plutôt compris qu'il s'agissait d'une manière plus simple d'accéder à la "signature" d'une fonction via le module inspect, pas vraiment d'une adoption des "signatures".

      • [^] # Re: Typos et signatures en python

        Posté par  . Évalué à 1.

        Je pensais plus à la 3107, mais elle est dans le même esprit. Les annotations de cette PEP sont plus générales que des types, mais il me semble que les types sont l'usage le plus immédiat.

    • [^] # Re: Typos et signatures en python

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

      Merci, les typos sont corrigées.

  • # Fibo

    Posté par  . Évalué à 1.

    Quand j'ai lu :

    La spécificité la plus marquante de Haskell, c'est qu'il s'agit d'un langage fonctionnel pur : une fonction appelée avec les mêmes arguments renverra toujours le même résultat,

    je me suis dit : chouette, je vais essayer fib 100. Mais, je m'attendais à ce qu'il stocke dans un cache une partie du résultat des fonctions en interne pour gagner du temps puisque avec des paramètres donnés, il n'y a qu'un résultat. Apparemment non, en tous cas pas par défaut.

    Du coup j'ai essayé en template C++ pour le faire compiler par le compilateur. C'est efficace :

    #include <iostream>
    
    using namespace std
    
    template <unsigned long long n> struct fibo {
      static const double result = fibo<n-1> + fibo <n-2>;
    };
    
    template <> struct fibo <1> {
      static const double result = 1.0;
    };
    
    template <> struct fibo <0> {
      static const double result = 0.0;
    };
    
    int main ()
    {
      cout << "Resultat 1000 : " << fibo<1000>::result << endl;
    
      return 0;
    }
    • [^] # Re: Fibo

      Posté par  . Évalué à 8.

      Il faut le demander explicitement : http://www.haskell.org/haskellwiki/Memoization

    • [^] # Re: Fibo

      Posté par  . Évalué à 7.

      Effectivement, il n'y a pas de « cache des résultats » (mémoïsation dans les termes de l'art), parce que savoir si c'est une bonne idée d'en faire un pour une fonction donnée est indécidable. Cependant, la version liste infinie qui est donnée est mémoïsée par le simple fait de produire une structure de données entière:

      fibs :: [Int]
      fibs = 0:1:(zipWith (+) fibs (tail fibs))
      
      nthFib :: Int -> Int
      nthFib n = fibs !! n
      
      main :: IO ()
      main = do
          putStrLn "entrez la valeur de 1000"
          mille <- readLn
          print (nthFib mille)

      En plus, elle n'oblige pas à fixer 1000 à la compilation.

      • [^] # Re: Fibo

        Posté par  . Évalué à 2.

        mémoïsation

        Merci, je ne connaissais pas le terme. La solution avec la liste, j'y avais pensé, mais j'avais fait une implémentation naïve. Du genre :

        fibo n = if n < 2 n else fibo (n-1) + fibo(n-2)

        qui ne m'a demandé que très peu de temps, malgré le fait que je ne connaisse pas le langage.

        Du fait que le langage soit purement fonctionnel, je m'attendais naïvement à ce que le runtime gère une zone mémoire de cache automatiquement ou avec une option de compilation.

        Le C++ était juste un exemple de template, le compilateur stocke toute les valeurs intermédiaires, et le linkeur ne retient que celle dont il a besoin. Je trouvais ça intéressant. Mais c'est évident que faire une version avec cache automatique en c++ demande également plus qu'une simple implémentation naïve. Ou alors en itératif.

        En tous cas, la présentation ici donne envie d'essayer plus avant ce langage.

        • [^] # Re: Fibo

          Posté par  . Évalué à 3.

          mémoïsation

          Merci, je ne connaissais pas le terme.

          Tu trouveras beaucoup plus de références avec l'anglais memoization.

      • [^] # Re: Fibo

        Posté par  . Évalué à 3. Dernière modification le 16 avril 2014 à 02:07.

        Ou, plus sobrement :

        fibs   = scanl (*) 1 [1..]
        nthFib = (fibs !!)
        

        On peut vérifier que ça mémoïse bien les résultats dans ghci :

        ghci> :sprint fibs
        fibs = _
        ghci> nthFib 3
        6
        ghci> :sprint fibs
        fibs = 1 : 1 : 2 : 6 : _
        ghci> nthFib 8
        40320
        ghci> :sprint fibs
        fibs = 1 : 1 : 2 : 6 : 24 : 120 : 720 : 5040 : 40320 : _
        
        • [^] # Re: Fibo

          Posté par  . Évalué à 10.

          C'est plus sobre certes, mais ça donne les factorielles plutôt que la suite de fibonacci.

    • [^] # Re: Fibo

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

      Le problème avec la mémoïsation automatique est de déterminer quand la faire. Il y a un compromis à trouver entre la vitesse d'exécution et l'espace mémoire (comme d'hab), pour quelles fonctions, selon quels paramètres en entrée, etc.

      La version avec l'utilisation d'une liste infinie doit normalement éviter de calculer plusieurs fois le même élement et le garbage collector se charge de supprimer les éléments au début de la liste qui ne sont plus nécessaires.

      Pour ton exemple C++, outre le fait qu'il manque des "::result", tu as conscience que c'est exécuté à la compilation et donc je suis moyennement surpris qu'afficher une constante soit efficace à l'exécution.

      Après l'exemple de la suite de Fibonacci n'est pas terrible puisque personne ne se sert d'Haskell pour calculer ça. Il y a tellement de choses plus intéressantes. Quelques exemples :

      1) Happstack (ou Yesod) pour intégrer un serveur web en quelques lignes dans une appli. L'analyse de l'url est type-safe, avec les modules de génération de HTML (comme blaze-html) on génère forcément des pages well-formed (vérifié statiquement par le système de types), en étant sûrs de ne renvoyer de façon non intentionnelle de chaînes de caractères qui ne soient pas converties en HTML ("échappées"), etc.

      2) Intégration de la mémoire transactionnelle. Pas besoin de se prendre la tête à gérer des mutexes ou autres. L'exemple classique du transfert d'argent entre 2 comptes :

      transfertPognon a b somme = atomically $ do
        courantA <- readTVar a
        when (courantA < somme) retry
        courantB <- readTVar b
        writeTVar a (courantA - somme)
        writeTVar b (courantB + somme)

      Le gros avantage c'est que c'est composable (pas besoin de prendre de mutexes dans l'ordre, etc.).

      3) QuickCheck pour tester automatiquement des fonctions en fonction de leurs types. Par exemple si vous écrivez une fonction de tri de caractères et que vous vouliez voir si elle a l'air conforme à un tri classique :

      let myCompare x y = case (x,y) of { ('a','b') -> GT; _ -> compare x y }
          mySort = sortBy myCompare
      ghci> quickCheck ((\s -> sort s == mySort s) :: String -> Bool)
      *** Failed! Falsifiable (after 52 tests and 9 shrinks):    
      "ab"

      Je vous invite à découvrir Haskell si ça n'est pas déjà fait. L'apprentissage est peut-être un peu difficile mais ça en vaut la peine.

      • [^] # Re: Fibo

        Posté par  . Évalué à 3.

        Pour ton exemple C++, outre le fait qu'il manque des "::result",

        oups.

        tu as conscience que c'est exécuté à la compilation et donc je suis moyennement surpris qu'afficher une constante soit efficace à l'exécution.

        Oui, c'est le temps de compilation qui aurait pu exploser. Ce n'est pas le cas car au fur et à mesure, les constantes ont leur valeur. A la fin, il ne garde que celle dont il a besoin.

        Je vous invite à découvrir Haskell si ça n'est pas déjà fait. L'apprentissage est peut-être un peu difficile mais ça en vaut la peine.

        Pour le vous, je suis vieux, mais ce n'est pas la peine de me le rappeler ;-)

        J'ai bien compris le principe de mémoire transactionnelle, mais pas la syntaxe de l'exemple.

        • [^] # Re: Fibo

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

          J'aurais du écrire "j'invite tout le monde" au lieu de "je vous invite", ce n'était pas du vouvoiement ;)

          Pour la syntaxe de l'exemple utilisant la mémoire transactionnelle, c'est la façon dont on écrit du code "impératif" en Haskell avec la do-notation. Donc ça se lit comme on lirait du C ou autre :

          -- On définit une fonction `transfertPognon` qui prend 3 paramètres (a,b,somme). Les types sont inférés.
          -- `atomically` permet d'exécuter ce qui lui est passé en paramètre dans une transaction. On ne peut pas faire d'IO dans une transaction donc elle est forcément pure (donc on peut la rejouer, etc.). Quand on accède aux variables transactionnelles, les écritures sont invisibles de l'extérieur tant que la transaction n'est pas "validée" ('commit').
          transfertPognon a b somme = atomically $ do 
          
             -- on lit la valeur stockée dans "a" qui est une variable de type `TVar Int` (donc qui ne peut être lue et écrite qu'avec readTVar et writeTVar respectivement au sein d'une transaction) et on l'affecte à `courantA`
             courantA <- readTVar a
          
             -- si le compte a n'a pas suffisamment d'argent, on appelle `retry`, ce qui aura pour effet de recommencer la transaction au début (donc de relire la valeur de `a`). Pour éviter de tourner pour rien comme sur un spinlock, le thread est bloqué jusqu'à ce qu'une des valeurs lues ait été modifiée (donc ici `a` vu qu'on a lu que cette variable). Pour rappel, les threads haskell sont des threads en espace utilisateur (green threads) donc ça ne coûte pas très cher.
             when (courantA < somme) retry 
          
             -- on lit `b` puis on écrit les 2 nouveaux montants dans les comptes
             courantB <- readTVar b 
             writeTVar a (courantA - somme) 
             writeTVar b (courantB + somme)

          Pour montrer comment on peut composer des fonctions qui utilisent la mémoire transactionnelle, on peut introduire deux fonctions retirer et verser pour enlever ou ajouter de l'argent à un compte donné :

          retirer compte somme = do
            courant <- readTVar compte
            when (courant < somme) retry
            writeTVar compte (courant - somme)
          
          verser compte somme = do
            courant <- readTVar compte
            writeTVar compte (courant + somme)
          
          transferer source dest somme = do
            retirer source somme
            verser dest somme
          
          transfertPognon a b somme = atomically (transferer a b somme)
      • [^] # Re: Fibo

        Posté par  . Évalué à 6. Dernière modification le 15 avril 2014 à 09:37.

        L'exemple classique du transfert d'argent entre 2 comptes :

        transfertPognon a b somme = atomically $ do
          courantA <- readTVar a
          when (courantA < somme) retry
          courantB <- readTVar b
          writeTVar a (courantA - somme)
          writeTVar b (courantB + somme)
        

        C'est niveau CS 101 mais tu devrais l'envoyer aux Jean Kevin de la nouvelle finance mondiale: Les exchanges Bitcoins. Ca fait au moins deux qui ferment et se font piller sur exactement cet exemple, et pleins d'autres ont eu le soucis.

        Quand tu te dis que Bitcoin remet en cause tous les processus de consistance, de sécurité et d'exposition aux risque des banques/exchange classiques par ce l'eventual consistancy ça ne marche pas des masses sur des transaction anonymes. Et quand tu vois le niveau des gars qui arrivent à se vautrer sur une bête balance de compte… :P

        • [^] # Re: Fibo

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

          Ouch, effectivement ça craint.

          Pour leur défense, c'est vrai que les langages de haut-niveau ne sont pas vraiment mis en avant dans les cursus informatique (en tout cas le mien). On dégoutte les élèves en leur faisant faire de la programmation impérative en LISP et en leur disant que c'est ça la programmation fonctionnelle, on leur vend Java comme étant le top du haut-niveau et ils sont mûrs pour la SSII.

          Donc ne parlons même pas de mémoire transactionnelle, de modèle data-flow, de modèle d'acteur, etc.

        • [^] # Re: Fibo

          Posté par  . Évalué à 5.

          […] Les exchanges Bitcoins.

          Les échanges Bitcoin/de Bitcoins.

          les processus de consistance

          Les processus de cohérence. (consistance veut dire tout à faire autre chose en français)

          […] exposition aux risque des banques/exchange classiques par ce l'eventual consistancy ça ne marche pas des masses sur des transaction anonymes.

          exposition aux risques des banques/échanges classiques parce que la «cohérence finale» ça ne marche pas des masses sur des transactions anonymes.

          Écrit en Bépo selon l’orthographe de 1990

          • [^] # Re: Fibo

            Posté par  . Évalué à 2.

            Les échanges Bitcoin/de Bitcoins.

            Non. Là il parle bien des exchanges (tu veux de la typographie ? En italique, j'ai le droit de mettre des mots étrangers :p ), les places de marché comme MtGox.

            • [^] # Re: Fibo

              Posté par  . Évalué à 2.

              Une bourse alors?

              Écrit en Bépo selon l’orthographe de 1990

              • [^] # Re: Fibo

                Posté par  . Évalué à 3. Dernière modification le 16 avril 2014 à 00:11.

                C'est bien tu as compris tous les mots. Tu es maintenant prêt pour créer ton exchange Bitcoin, cours chercher ton MongoDB et révolutionne l'histoire des catastrophes financières ;)

    • [^] # Re: Fibo

      Posté par  . Évalué à 6. Dernière modification le 15 avril 2014 à 20:51.

      À noter (pour rebondir sur la deuxième partie du commentaire, le code, qui n’a presque rien à voir avec la première partie, la mémoïsation) qu’on peut aussi faire de la meta-programmation grâce à l’extension Template Haskell. Une extension très utilisée, qui permet aussi de faire de la meta-programmation à l’exécution (!) et qui voit sa sûreté renforcée grâce au typage dans cette nouvelle version. Probablement l’une des avancées fondamentales de GHC 7.8.

    • [^] # Re: Fibo

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

      Du coup j'ai essayé en template C++ pour le faire compiler par le compilateur. C'est efficace

      Oui mais ça n'a rien à voir avec l'évaluation de la fonction pendant l'éxécution du programme.

      Tu peux aussi directement écrire la valeur à la main, ou utiliser un tableau avec les premières valeurs.

  • # La 7.8.2 est déjà dans les bacs...

    Posté par  . Évalué à 5.

    et vivement recommandée puisque contenant le correctif pour une régression du vérificateur de type (bug 8978).

  • # Bizarre, aucun commentaire ou comparaison avec Ocaml.

    Posté par  . Évalué à 2. Dernière modification le 15 avril 2014 à 16:28.

    Saluton samideanoj! Vraiment surprenant de ne voir jusqu'à présent aucun commentaire comparant Haskell et Ocaml ?

  • # cat troll.hs

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

    La spécificité la plus marquante de Haskell, c'est qu'il s'agit d'un langage fonctionnel pur : une fonction appelée avec les mêmes arguments renverra toujours le même résultat, contrairement à une fonction comme input(message) en python.

    J'ai essayé avec le code suivant :

    import System.Random
    foo x = do
      r <- randomIO
      return (x + r)

    Mais à chaque fois que j'appelle la fonction foo avec le paramètre "3", j'obtiens un résultat différent. Mon GHC est-il cassé ?

    • [^] # Re: cat troll.hs

      Posté par  . Évalué à 6.

      Si le langage était vraiment pur, tu n’aurais ni affichage pour voir le résultat, ni rien du tout, le simple fait d’exécuter du code sur un ordinateur a un effet (par exemple en cherchant à allouer une grande quantité de mémoire dans un système plus ou moins chargé).

      En Haskell, le code est par défaut pur, et le code impur est très visiblement marqué comme tel. Dans ton exemple, foo est de type Num a => a -> IO a. On travaille donc dans la monade IO, qui a été spécialement conçu pour gérer les effets de bords. Mais il est vrai que la formulation de la dépêche est quelque peu obscure. L’équivalent Haskell d’une entrée comme input en Python est forcément impur.

      D’un point de vue moins pragmatique, tu te trompes. Ta fonction foo n’a pas renvoyé un résultat différent à chaque fois. Elle a toujours renvoyé une Num a => IO a. Le fait qu’a un moment donné quelque chose (une valeur numérique, probablement un Int) ait été généré à partir de cette IO n’affecte pas la pureté de foo.

      • [^] # Re: cat troll.hs

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

        L’équivalent Haskell d’une entrée comme input en Python est forcément impur.

        Oui mais justement le gros intérêt de Haskell en comparaison avec d'autres langages comme OCaml par exemple, c'est qu'ils n'ont pas transigé sur la pureté pour intégrer les IOs. Donc on conserve toutes les propriétés liées à la pureté.

        (Brièvement) pour ceux qui se demandent comment ça marche (j'avais présenté ça ici) :

        Si on avait une fonction input qui renvoyait une chaîne en faisant un effet de bord (lecture de ce que tape l'utilisateur) et ayant pour type input :: String. Supposons qu'on fasse plusieurs lectures :

        main = let
            nom = input
            prenom = input
            age = input
          in print ("salut " ++ prenom ++ " " ++ nom)

        Comme Haskell est paresseux, prenom et nom seraient évalués seulement quand on aurait besoin d'afficher la chaîne (donc dans quel ordre ?) et age ne serait jamais évalué. Comme il y a la transparence référentielle et qu'on appelle input avec les mêmes paramètres (i.e. aucun), on peut dire que nom = prenom = age = input. Bref ça ne marche pas, d'autant plus que comme print ne renvoie rien, on n'a jamais besoin de l'évaluer donc il ne se passerait rien.

        On s'en sort en disant que input prend en fait un paramètre, l'état du monde extérieur à Haskell et qu'en plus d'une chaîne il renvoie l'état du monde extérieur après avoir fait l'IO. Donc son type est en fait input :: RealWorld -> (RealWorld,String). De la même façon, on suppose que main reçoit l'état du monde extérieur avant le lancement du programme et renvoie l'état du monde extérieur après l'exécution. Maintenant il ne reste plus qu'à chaîner tout ça :

        main w1 = let
             (w2,nom) = input w1
             (w3,prenom) = input w2
             (w4,age) = input w3
           in print ("salut " ++ prenom ++ " " ++ nom) w4

        Donc là on est sûr que pour obtenir l'état du monde renvoyé par main, il faut évaluer print... w4, donc pour obtenir w4 il faut évaluer input w3 et ainsi de suite. Notez qu'on aurait pu faire autrement et dire que chaque fonction qui effectue une IO prend en paramètre une fonction qui va utiliser son résultat (continuation passing style, CPS) et on aurait obtenu Node.js, mais Haskell est un langage moderne qui a remplacé cette approche en 1992 (lancer de troll réussi ?).

        On peut introduire un alias pour simplifier l'écriture des types :

        type IO a = RealWorld -> (RealWorld,a)

        Donc on aurait input :: IO String, print :: String -> IO () (où () veut dire void) et main :: IO ().

        Le problème avec les variables world explicites est que si le programmeur fait n'importe quoi avec (s'il en utilise une plusieurs fois, etc.) ça n'est plus déterministe et c'est n'importe quoi. Donc soit on détecte statiquement que l'utilisateur fait n'importe quoi (ce qu'ils ont fait dans le langage Clean plus tard), soit on cache l'utilisation de ces variables en ne proposant que l'opérateur pour composer deux opérations qui font des IO (l'équivalent de ";" en C). Donc en Haskell c'est ça, on peut utiliser >>= pour composer deux opérations, avec (>>=) :: IO a -> (a -> IO b) -> IO b :

        main = input >>= \nom -> input >>= \prenom -> input >>= \age -> print ("salut " ++ prenom ++ " " ++ nom)

        Et comme c'est très moche, on utilise l'équivalent avec du sucre syntaxique (do-notation) :

        main = do
           nom <- input
           prenom <- input
           age <- input
           print ("salut " ++ prenom ++ " " ++ nom)

        Le principe et la notation se généralisent à d'autres cas où on cherche à "chaîner des opérations" (cf Monad), c'est pour ça qu'on la retrouve dans l'exemple utilisant la mémoire transactionnelle dans mon commentaire plus haut alors qu'il n'y a pas d'IOs.

        Bref au final c'est resté pur tout en faisant des IOs, c'est pour ça que certains parlent de "meilleur langage impératif du monde" comme le mentionne la dépêche. J'aurais dit "univers" mais bon. ;)

        • [^] # Re: cat troll.hs

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

          Merci pour ces explications, très claires.

          En anglais j'aime bien le texte suivant comme introduction aux monades:

          http://blog.enfranchisedmind.com/2007/08/a-monad-tutorial-for-ocaml/

          C'est très court et on comprend assez bien diverses applications de ce design pattern ou plutôt de ce patron applicatif.

          Question: que signifie cette notation \nom ?

          • [^] # Re: cat troll.hs

            Posté par  . Évalué à 3.

            Question: que signifie cette notation \nom ?

            \ représente le caractère λ, en référence au λ-calcul et à ses fonctions anonymes. Donc \nom -> "Salut " ++ nom est équivalent à \x -> "Salut " ++ x.

            Il existe une extension livrée avec GHC qui permet de compiler λnom → "Salut " ⧺ nom. On peut aussi configurer vim et emacs pour qu'ils affichent le code sous cette forme, tout en transmettant la version ASCII au compilateur.

      • [^] # Re: cat troll.hs

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

        D’un point de vue moins pragmatique, tu te trompes. Ta fonction foo n’a pas renvoyé un résultat différent à chaque fois. Elle a toujours renvoyé une Num a => IO a. Le fait qu’a un moment donné quelque chose (une valeur numérique, probablement un Int) ait été généré à partir de cette IO n’affecte pas la pureté de foo.

        La fonction n'est pas pure, car son résultat dépend d'un état caché, celui de la monade IO. Plus précisément, elle dépend de randomIO qui n'est pas pure et dont elle extrait une valeur qu'elle retourne. On en a pour preuve que le résultat est effectivement différent.

        Tu parles de toujours retourner Num a => IO a, mais c'est le type, non la valeur. Le type est habité par un grand nombre de valeurs, plusieurs desquelles peuvent être retournées par des appels successifs à foo, ce qui s'observe en pratique.

        (NB: on peut tout à fait avoir une fonction pure dans IO:

        bar x = do
          inutile <- return 5
          return x

        Cette fonction retournera toujours la même valeur, et n'effectue aucun effet de bord observable, en dépit du fait qu'elle est "marquée".)

        • [^] # Re: cat troll.hs

          Posté par  . Évalué à 4. Dernière modification le 16 avril 2014 à 01:47.

          Le post de Sylvain Henry explique bien pourquoi justement cette IO n’a pas de type caché et est bien pure. La fonction randomIO retourne une IO qui n’est pas la valeur du nombre aléatoire, mais une monade qui indique comment obtenir une valeur. Mais ce comment reste toujours le même quelque soit le nombre de fois que tu appelles foo ou randomIO.

          On peut voir ce comment en action en n’appellant qu’une seule fois randomIO, afin d’obtenir une seule valeur de retour (de type IO double par exemple), et en l’invoquant plusieurs fois.

          ghci> import System.Random
          ghci> let f = randomIO :: IO Double
          ghci> f
          9.246329697709554e-3
          ghci> f
          0.954075333075334
          

          On peut faire de même avec ta fonction foo, ne l’appeller qu’une seule fois, et se servir de la monade retournée pour obtenir plusieurs valeurs. IO ne fournit qu’une méthode pour accéder au monde, mais ne modifie pas le monde en elle même.

          Le tout est expliqué et formalisé par l’auteur du langage, Simon Peyton Jones, dans son célèbre Tackling the awkward squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell.

          • [^] # Re: cat troll.hs

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

            randomIO ne retourne pas une monade (les monades ne sont pas des citoyens de première classe en Haskell). randomIO, fonction zéro-adique, retourne une valeur dans la monade IO. Et c'est différent.

            Quand tu fais f = randomIO tu n'évalues pas randomIO, tu crées un nouveau binding vers la fonction randomIO (en gros, un alias). En évaluant ta variable f plusieurs fois, tu appelles la fonction zéro-adique randomIO autant de fois, et tu obtiens donc des résultats différents (résultats qui sont extraits par <- et non par =).

            Ce comportement se comprend mieux si on compare à la monade State qui fonctionne de manière similaire, mais à laquelle tu fournies l'objet de type World explicitement. Tu peux écrire une fonction randomState qui se comporte de façon similaire. Cette fonction serait pure. Mais avec IO, tu es incapable de reproduire l'exécution, car tu es incapable de fournir ou extraire le monde (contrairement à la monade State). En pratique, c'est donc impure. C'est important, car on programme plus en pratique qu'en théorie.

            • [^] # Re: cat troll.hs

              Posté par  . Évalué à 2.

              les monades ne sont pas des citoyens de première classe en Haskell

              WTF ? Et surtout quelle est la suite ? Les nombres n’existent pas en Haskell ? Les listes non plus ? La syntaxe do non plus ?

              randomIO, fonction zéro-adique
              […] un nouveau binding vers la fonction randomIO
              […] tu appelles la fonction zéro-adique

              Non. Ce concept n’existe pas en Haskell.
              Ce n’est pas une fonction, c’est une valeur de type Num a => IO a (cf le tuto de SPJ, et les autres commentaires).

              (Et là, je suis sûr d’avoir marché dans le troll pourtant bien annoncé dans le sujet du thread.)

              • [^] # Re: cat troll.hs

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

                Tu peux faire f :: m a -> int mais pas "g :: m -> b" où m est une monade, ni "h :: m a -> m".
                Pour moi, ça fait que les monades ne sont pas citoyens de première classe.

                • [^] # Re: cat troll.hs

                  Posté par  . Évalué à 4. Dernière modification le 01 mai 2014 à 18:17.

                  C’est parce qu’on ne travaille pas dans un système de types simpliste, comme le lambda calcul simplement typé. Il y a plusieurs kinds, qui sont des constructeurs de types (juste deux en Haskell, ceux d’arité 0 ou 1, les autres sont obtenus par curryfication). Ici tes Int et b sont de kind *, mais m est de kind * -> *, j’imagine. Plus d’infos dans le classique TAPL de Pierce,

                  Pour simplifier, c’est pareil qu’en C, tu ne peux pas écrire qu’une fonction prend en paramètre un tableau, sans spécifier le type de ses éléments (pas de func (int x, [] y)). En Haskell, tu peux quand même généraliser avec forall a. func :: Int -> Monad a par exemple. (En C, on doit outrepasser le système de types par des casts).

        • [^] # Re: cat troll.hs

          Posté par  . Évalué à 2.

          Une valeur de type IO a n'est pas une valeur «marquée impure», mais une action non pure produisant une valeur de type a. En théorie, IO n'est pas un conteneur contenant une valeur de type a. La notion de foncteur, et par extension de monade, est plus générale que celle de conteneur.

          C'est seulement lorsque cette action est exécutée dans le main ou l'invite de ghci que la valeur aléatoire est générée. Dans le reste du programme, il n'y a que des fonctions pures qui combinent des actions monadiques avec >>= pour assembler l'action non pure IO () du main.

          En pratique, (mais cela importe peu) cela est implémentée avec un passage de bâton RealWorld, comme l'a montré Sylvain Henry. Il est également possible d’altérer cette mécanique cachée avec des fonctions comme unsafePerformIO :: IO a -> a. Mais comme son nom l'indique, elle peut potentiellement supprimer les garanties de la transparence référentielle.

  • # Applications

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

    Une critique récurrente de Haskell — surtout en comparaison à OCaml — est le manque d'applications industrielles effectives.

    Pour OCaml, celles-ci sont anciennes (Microsoft, Dassault, Intel) toujours plus nombreuses mais quid de Haskell? Haskell est utilisé en interne par le crédit suisse (en tout cas ils recrutent des programmeurs haskell) pour faire du pricing et du risk management. Mais y-a-t-il un catalogue plus étoffé d'utilisateurs industriels?

    Mes amis haskelleurs disent qu'un gros défaut du langage est la difficulté à optimiser et profiler le programme. (Si je comprends bien, comme l'évaluation n'est pas stricte, il n'y aurait pas de correspondance claire entre le code source du programme et le flot des traitements effectués par le programme.) Existe-t-il des méthodes efficaces pour cela? En regard de Haskell, OCaml est très simple et on suit très facilement son code source original en lisant le code assembleur généré.

Suivre le flux des commentaires

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