• # Autres changements interessants

    Posté par (site web personnel) . En réponse à la dépêche Le compilateur GHC Haskell en version 8.0.1. É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.