J'ai pas trop de souci avec la verbosité d'ADA (Que je découvre en lisant ton code). Je suis plus embêté par le concept de devoir fournir un cas par défaut pour un type comme tu le laisse supposer en commentaire. Cela veut dire qu'ADA, si tu n'initialise pas explicitement ta valeur, va prendre le choix par défaut ? Je n'aime pas ;)
On a droit également à une exception si on tente d'accéder ou de modifier un champ alors que le discriminant n'est pas le bon :
Il n'y a pas de garantie statique sur ce genre de chose ? Ça c'est dommage.
Les range, c'est sympa que le langage propose cela de façon native, mais au final cela s'émule assez bien dans n'importe quel autre langage avec un peu de typage dépendant.
Par exemple, en Haskell :
{-# LANGUAGE KindSignatures, DataKinds, TypeApplications, ScopedTypeVariables #-}importGHC.TypeLitsimportControl.Exception(assert)importData.ProxynewtypeRange(a::Nat)(b::Nat)=RangeValIntegerderiving(Show)newValInRange::forallab.(KnownNata,KnownNatb)=>Integer->RangeabnewValInRangei=assert(i>=bMin&&i<=bMax)(RangeVali)wherebMin=natVal@aProxybMax=natVal@bProxy
C'est assez verbeux et incompréhensible pour l’œil non habitué, donc en gros, si on passe les import et LANGUAGE nécessaires. On a une définition de type Range, paramétrée par deux types "phantoms", bMin et bMax qui représentent les borne du range, mais ces valeurs sont stockées au niveau du type. Cependant la valeur stockée ne peut pas être contrainte, donc on stock un Integer dedans.
Tous le problème se résume maintenant à empêcher la création d'un Range sans passer par une procédure de vérification. Pour cela on n'exportera pas le constructeur RangeVal, à la place, on va exporter la fonction newValInRange. Son type est assez barbare, newValInRange :: forall a b. (KnownNat a, KnownNat b) => Integer -> Range a b, mais en gros elle prend un Integer et renvoie un Range a b. Le truc c'est que on va se servir du a et b attendus (au niveau du type) pour déduire la vérification au niveau valeur.
La ligne assert (i >= bMin && i <= bMax) (RangeVal i) se contente de renvoyer notre Integer bien encapsulé dans un RangeVal, mais après avoir testé qu'il est bien dans les bornes, en se servant de bMin = natVal @a Proxy pour transformer le type a, Integer au niveau du type, vers la valeur bMin, integer au niveau valeur.
Après cela s'utilise comme cela. Imaginons la fonction suivante qui accepte un Range 0 10 :
launchMissile::Range010->IO()launchMissiler=casegetValInRangerof0->putStrLn"Paix et harmonie"n->putStrLn(unwords(replicate(fromIntegern)"EXTERMINATE!"))
qui se sert entre autre de la fonction utilitaire getValInRange (RangeVal i) = i qui permet d'extraire l'Integer du range.
On note que l’inférence de type fait son boulot et déduit tout seul que le résultat de newValInRange doit être un Range 0 10.
Mais bon, c'est une usine à gaz de typage dépendant pour au final avoir une erreur au runtime, donc c'est assez inutile, on pourrait remplacer la même chose par un bête check dans la fonction launchMissile.
Ce qui est par contre intéressant c'est quand on peut faire tout ce processus à la compilation. Par exemple, une fonction launchMissile qui connait son nombre de missile à la compilation et peut faire des vérifications statiques dessus :
launchMissile' :: forall (n :: Nat). (KnownNat n, 0 <= n, n <= 10) => IO ()
launchMissile' = case natVal @n Proxy of
0 -> putStrLn "Paix et harmonie"
n -> putStrLn (unwords (replicate (fromInteger n) "EXTERMINATE!"))
(Cette solution demande les extensions TypeOperators, ConstraintKinds, TypeFamilies, AllowAmbiguousTypes, rien que ça, et aussi le package typelits-witnesses et l'import de 'GHC.TypeLits.CompareetData.Type.Bool.) IcilaunchMissile'est une fonction qui est paramétrée parnà la compilation, donc c'est à la compilation que l'erreur surn` pétera.
Bon, çà c'est vachement plus simple en C++, qui gère super bien le typage dépendant sur les entiers et vivent les constexpr :
template<intn>voidlaunchMissile2(){static_assert(n>=0&&n<=10,"WTF!");if(n==0)std::cout<<"Paix et harmonie\n"<<std::endl;elsefor(autoi=0u;i<n;++i){std::cout<<"EXTERMINATE!\n";}}constexprintfact(constintn){intres=1;for(inti=1;i<=n;i++){res*=i;}returnres;}intmain(){launchMissile2<fact(3)>();// lance 3! == 6 missiles}
(Bon, le c++ c'est bon des fois, constexpr déchire très souvent, jusqu'à ce que tu veuilles créer des valeurs plus poussées au niveau du type. ;)
[^] # Re: Solution à base de types variants en ADA
Posté par Guillaum (site web personnel) . En réponse à la dépêche Sortie de GHC 8.0.2 et une petite histoire de typage statique. Évalué à 4.
J'ai pas trop de souci avec la verbosité d'ADA (Que je découvre en lisant ton code). Je suis plus embêté par le concept de devoir fournir un cas par défaut pour un type comme tu le laisse supposer en commentaire. Cela veut dire qu'ADA, si tu n'initialise pas explicitement ta valeur, va prendre le choix par défaut ? Je n'aime pas ;)
Il n'y a pas de garantie statique sur ce genre de chose ? Ça c'est dommage.
Les
range, c'est sympa que le langage propose cela de façon native, mais au final cela s'émule assez bien dans n'importe quel autre langage avec un peu de typage dépendant.Par exemple, en Haskell :
C'est assez verbeux et incompréhensible pour l’œil non habitué, donc en gros, si on passe les
importetLANGUAGEnécessaires. On a une définition de typeRange, paramétrée par deux types "phantoms",bMinetbMaxqui représentent les borne du range, mais ces valeurs sont stockées au niveau du type. Cependant la valeur stockée ne peut pas être contrainte, donc on stock unIntegerdedans.Tous le problème se résume maintenant à empêcher la création d'un
Rangesans passer par une procédure de vérification. Pour cela on n'exportera pas le constructeurRangeVal, à la place, on va exporter la fonctionnewValInRange. Son type est assez barbare,newValInRange :: forall a b. (KnownNat a, KnownNat b) => Integer -> Range a b, mais en gros elle prend unIntegeret renvoie unRange a b. Le truc c'est que on va se servir duaetbattendus (au niveau du type) pour déduire la vérification au niveau valeur.La ligne
assert (i >= bMin && i <= bMax) (RangeVal i)se contente de renvoyer notreIntegerbien encapsulé dans unRangeVal, mais après avoir testé qu'il est bien dans les bornes, en se servant debMin = natVal @a Proxypour transformer le typea,Integerau niveau du type, vers la valeurbMin, integer au niveau valeur.Après cela s'utilise comme cela. Imaginons la fonction suivante qui accepte un
Range 0 10:qui se sert entre autre de la fonction utilitaire
getValInRange (RangeVal i) = iqui permet d'extraire l'Integerdu range.Alors, nous obtenons :
On note que l’inférence de type fait son boulot et déduit tout seul que le résultat de
newValInRangedoit être unRange 0 10.Mais bon, c'est une usine à gaz de typage dépendant pour au final avoir une erreur au runtime, donc c'est assez inutile, on pourrait remplacer la même chose par un bête check dans la fonction
launchMissile.Ce qui est par contre intéressant c'est quand on peut faire tout ce processus à la compilation. Par exemple, une fonction
launchMissilequi connait son nombre de missile à la compilation et peut faire des vérifications statiques dessus :(Cette solution demande les extensions
TypeOperators, ConstraintKinds, TypeFamilies, AllowAmbiguousTypes, rien que ça, et aussi le packagetypelits-witnesseset l'import de 'GHC.TypeLits.CompareetData.Type.Bool.) IcilaunchMissile'est une fonction qui est paramétrée parnà la compilation, donc c'est à la compilation que l'erreur surn` pétera.Bon, çà c'est vachement plus simple en C++, qui gère super bien le typage dépendant sur les entiers et vivent les
constexpr:(Bon, le c++ c'est bon des fois,
constexprdéchire très souvent, jusqu'à ce que tu veuilles créer des valeurs plus poussées au niveau du type. ;)Aller, dodo.