Qu'entends-tu par union sécurisée? Une union avec de la "RTTI"?
Exemple :
dataObject=SquareFloat|RectangleFloatFloat
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 :
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<typenameT>intfind(conststd::vector<T>&v,constTvalue);// 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<typenameT>boolfind(conststd::vector<T>&v,constTvalue,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 :
dataMaybet=Nothing|Justt
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->MaybeInt
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(findmaListemaValeur)ofJustoffset->"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 :
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)<$>(getResultv)
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 :
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 :
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 ;)
[^] # Re: Moi == pas doué, je suppose....
Posté par Guillaum (site web personnel) . En réponse au journal Haskell et le tri. Évalué à 5.
Exemple :
Ici, le type
Objectpeut-être soit un carréSquareavec un argumentFloat(le coté), soit unRectangleavec deux arguments Float (les deux cotés). Pour faire la difference entre les différents objets, tu es forcé de "pattern match", examples :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
findqui te renvoie la position dans un tableau d'un élément.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 :
Ici c'est un type polymorphique paramétré par
t. Il peut donc contenir soitNothing, soitJustune valeur de type t.Maintenant la fonction
find: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 :
Ici la branche
Nothingn'a pas le même scope que l'autre branche, ainsi tu ne peux pas utiliseroffsetdans 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.
Rust est assez extra en effet. C'est pas aussi beau et pure que Haskell à mon sens, mais c'est un beau compromis.
C'était les exemples de compositions. Continuons sur le
Maybeque j'ai décris quelques lignes plus haut. On va supposer que nous avons une fonctiongetResult :: Int -> Maybe Intqui renvoie peut-être un Int. Tu veux renvoyer unMaybecontenant le résultat multiplié par deux ou Nothing si le résultat n'existe pas. Une implementation naive serait :Cela devient vite lourd. Mais tu as pleins de fonction pour composer directement dans le
Maybe, comme la fonctionfmapou<$>ce qui à la place donne de façon totalement équivalente :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 uneStringprésentant l'employé, ou Nothing si ce n'est pas possible :C'est lourd. Il y a d'autres solutions, mais rien de bien sympathique, sauf la syntaxe
doqui permet d'exprimer cela :Ici le
<-signifie de sortir le résultat duMaybeoù d’arrêter le calcul directement si celui-ci contient unNothing. Cette syntaxe est polymorphique et dépend du type manipulé à condition que celui-ci correspond à une interface (la fameuseMonad). Comme cela tu peux faire une API très propre, tu pourrais imaginer une API de réseau :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
getRessourcequi 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.Oui ;) Et si on pouvait avoir des vrais enums typés (i.e: pas de faux int) ce serait mieux ;)