• [^] # Re: Moi == pas doué, je suppose....

    Posté par (site web personnel) . En réponse au journal Haskell et le tri. Évalué à 5.

    Qu'entends-tu par union sécurisée? Une union avec de la "RTTI"?

    Exemple :

    data Object = Square Float | Rectangle Float Float

    Ici, le type Object peut-être soit un carré Square avec un argument Float (le coté), soit un Rectangle avec deux arguments Float (les deux cotés). Pour faire la difference entre les différents objets, tu es forcé de "pattern match", examples :

    area :: Object -> Float
    area (Square c) = c * c
    area (Rectangle c0 c1) = c0 * c1
    -- ou
    area' :: Object -> Float
    area' object = case object of
     (Square c) -> c * c
     (Rectangle c0 c1) = c0 * c1

    Ici tu ne peux pas te tromper, tu ne peux pas utiliser un carré à la place d'un rectangle.

    Example plus avancé, si tu veux faire une fonction qui peut ne pas renvoyer la valeur, tu fais sois appel à une valeur "sentinelle" soit tu passe par un booléen en retour. Exemple avec une fonction find qui te renvoie la position dans un tableau d'un élément.

    // Valeur sentinelle, c'est l'approche stl qui renvoie std::end()
    // Si l’élément est trouvé, cela renvoi son index dans le vecteur
    // Sinon, cela renvoie "-1"
    template<typename T>
    int find(const std::vector<T> &v, const T value);
    // Approche booléenne
    // Si l’élément est trouvé, cela renvoie true et met l'index dans `result`
    // Sinon, cela renvoi false et la valeur de result est indéterminé
    template<typename T>
    bool find(const std::vector<T> &v, const T value, int &result);

    Chaque approche a ses limitation. L'approche "sentinelle" fait que tu peux utiliser une valeur qui n'a aucun sens et l'approche Booléen fait que tu peux utiliser par erreur une valeur non initialisée.

    En Haskell (et Rust, et OCaml, et sans doute d'autres), tu vas utiliser une "union typée safe" représentant le résultat où son absence :

    data Maybe t = Nothing | Just t

    Ici c'est un type polymorphique paramétré par t. Il peut donc contenir soit Nothing, soit Just une valeur de type t.

    Maintenant la fonction find :

    -- Cette fonction prend une liste de t `[t]`, une valeur
    -- de type `t` à trouver et renvoie `Just offset` si il est trouvé, ou alors `Nothing`
    find :: [t] -> t -> Maybe Int

    Lors de l'utilisation, tu n'as pas de valeur "sentinelle" abstraite ni de valeur non initialisée et c'est garantie par la compilation :

    case (find maListe maValeur) of
     Just offset -> "La valeur est trouvée à l'offset: " ++ show (offset)
     Nothing -> "Valeur non trouvée"

    Ici la branche Nothing n'a pas le même scope que l'autre branche, ainsi tu ne peux pas utiliser offset dans la mauvaise branche.

    Note que tu peux faire quelque chose de proche avec les exceptions dans de nombreux langage, mais ce n'est pas aussi propre car l'exception n’apparaît pas dans le type de la fonction et n'est pas forcement vérifiée par le compilateur.

    Le prochain langage que j'apprendrais, ce sera sûrement Rust, il a pas mal de points intéressants à ce qu'il semble. (pas de GC, se base sur des techniques éprouvées, multi-paradigmes, pas d'héritage direct du C)

    Rust est assez extra en effet. C'est pas aussi beau et pure que Haskell à mon sens, mais c'est un beau compromis.

    (Je fournis des examples si vous voulez).

    Je suis preneur, à titre de curiosité.

    C'était les exemples de compositions. Continuons sur le Maybe que j'ai décris quelques lignes plus haut. On va supposer que nous avons une fonction getResult :: Int -> Maybe Int qui renvoie peut-être un Int. Tu veux renvoyer un Maybe contenant le résultat multiplié par deux ou Nothing si le résultat n'existe pas. Une implementation naive serait :

    case (getResult v) of
     Just res -> Just (res * 2)
     Nothing -> Nothing

    Cela devient vite lourd. Mais tu as pleins de fonction pour composer directement dans le Maybe, comme la fonction fmap ou <$> ce qui à la place donne de façon totalement équivalente :

    (*2) <$> (getResult v)

    Cela te permet de remonter au cran d'au dessus la gestion de l’échec. Malheureusement cela peut vite devenir un peu l'horreur si tu dois combiner plusieurs fonctions. Example, tu as 3 fonctions pour gérer tes utilisateurs qui associe leur Id à leur nom, prénom et leur salaire. Toutes ces fonctions peuvent échouer pour une raison ou une autre. Tu veux une fonction qui connaissant un Id te retourne une String présentant l'employé, ou Nothing si ce n'est pas possible :

    getLastName :: Id -> Maybe String
    getFirstName :: Id -> Maybe String
    getSalary :: Id -> Maybe Int
    getGreetings :: Id -> Maybe String
    getGreetings id = case getFirstName id of
     Nothing -> Nothing
     Just firstName -> case getLastName id of
     Nothing -> Nothing
     Just lastName -> case getSalary id of
     Nothing -> Nothing
     Just salary -> Just ("Bonjour, " ++ firstName ++ " " ++ lastName ++ ". Vous gagnez " ++ (show salary) ++ " peanuts / jours.")

    C'est lourd. Il y a d'autres solutions, mais rien de bien sympathique, sauf la syntaxe do qui permet d'exprimer cela :

    getGreetings :: Id -> Maybe String
    getGreetings id = do
     firstName <- getFirstName id
     lastName <- getLastName id
     salary <- getSalary id
     Just ("Bonjour, " ++ firstName ++ " " ++ lastName ++ ". Vous gagnez " ++ (show salary) ++ " peanuts / jours.")

    Ici le <- signifie de sortir le résultat du Maybe où d’arrêter le calcul directement si celui-ci contient un Nothing. Cette syntaxe est polymorphique et dépend du type manipulé à condition que celui-ci correspond à une interface (la fameuse Monad). Comme cela tu peux faire une API très propre, tu pourrais imaginer une API de réseau :

    getRessource host port login password ressourceId = do
     ip <- resolveHost host
     connection <- connect ip port
     auth <- authenticate connection (login, password)
     downloadRessource connection auth ressourceId

    Sachant que chaque fonction peut échouer sur le chemin en renvoyant un type d'erreur, tu peux crée ta fonction de plus haut niveau getRessource qui te renverra soit l'erreur soit la ressource finale, sans devoir écrire la tuyauterie et l’enchaînement de cas pour gérer toutes les erreurs.

    Perso, je pense que quand on fait une fonction qui prend en paramètre un jeu de constantes, le plus simple est de définir une enum qui les définit, et si il s'avère qu'un utilisateur normal (du logiciel et non de la lib) peut impacter dessus via de la config, alors on file une fonction de conversion string<->enum.

    Oui ;) Et si on pouvait avoir des vrais enums typés (i.e: pas de faux int) ce serait mieux ;)