Je travaille dans un secteur où la sécurité est essentielle, et nos projets de logiciels sont généralement soumis à des exigences de sécurité, c'est-à-dire des choses dont nous devons démontrer que le logiciel les réalise avec un degré élevé de certitude. Il s'agit souvent d'exigences négatives, telles que " ne doit pas se corrompre plus fréquemment qu'une fois sur " (j'ajouterai que ces exigences proviennent d'exigences de sécurité de systèmes statistiques).
Une source de corruption est clairement les erreurs de codage, et j'aimerais utiliser le système de types Haskell pour exclure au moins certaines classes de ces erreurs. Quelque chose comme ça :
Tout d'abord, voici notre donnée critique qui ne doit pas être corrompue.
newtype Critical = Critical String
Maintenant, je veux stocker cet élément dans d'autres structures.
data Foo = Foo Integer Critical
data Bar = Bar String Critical
Je veux maintenant écrire une fonction de conversion de Foo en Bar qui soit garantie de ne pas toucher aux données critiques.
goodConvert, badConvert :: Foo -> Bar
goodConvert (Foo n c) = Bar (show n) c
badConvert (Foo n (Critical s)) = Bar (show n) (Critical $ "Bzzt - " ++ s)
Je veux que "goodConvert" vérifie le type, mais que "badConvert" échoue la vérification du type.
Évidemment, je ne peux pas importer le constructeur de Critical dans le module qui effectue la conversion. Mais ce serait bien mieux si je pouvais exprimer cette propriété dans le type, car alors je peux composer des fonctions qui sont garanties de préserver cette propriété.
J'ai essayé d'ajouter des types fantômes et des "forall" à divers endroits, mais cela n'aide pas.
Une chose qui pourrait fonctionner serait de ne pas exporter le constructeur Critical, et d'avoir alors
mkCritical :: String -> IO Critical
Comme le seul endroit où ces éléments de données critiques sont créés est dans les fonctions d'entrée, cela a un certain sens. Mais je préférerais une solution plus élégante et plus générale.
Modifier
Dans les commentaires, FUZxxl a suggéré de jeter un œil à Haskell sûr . Cela semble être la meilleure solution. Plutôt que d'ajouter un modificateur "pas de corruption" au niveau du type comme je le voulais à l'origine, il semble que vous puissiez le faire au niveau du module, comme ceci :
1 : Créez un module "Critical" qui exporte toutes les caractéristiques du type de données Critical, y compris son constructeur. Marquez ce module comme "unsafe" en mettant "{-# LANGUAGE Unsafe #-}" dans l'en-tête.
2 : Créer un module "SafeCritical" qui réexporte tout sauf le constructeur et toutes les autres fonctions qui pourraient être utilisées pour corrompre une valeur critique. Marquez ce module comme "digne de confiance".
3 : Marquez comme "sûrs" tous les modules qui doivent manipuler des valeurs critiques sans corruption. Ensuite, utilisez ceci pour démontrer que toute fonction importée comme "sûre" ne peut pas causer la corruption d'une valeur critique.
Il restera une petite minorité de code, comme le code d'entrée qui analyse les valeurs critiques, nécessitant une vérification supplémentaire. Nous ne pouvons pas éliminer ce code, mais la réduction du nombre de codes nécessitant une vérification détaillée est une victoire importante.
La méthode est basée sur le fait qu'une fonction ne peut inventer une nouvelle valeur que si une autre fonction la renvoie. Si une fonction ne reçoit qu'une seule valeur critique (comme dans la fonction "convert" ci-dessus), c'est la seule qu'elle peut retourner.
Une variante plus difficile du problème se présente lorsqu'une fonction possède deux ou plusieurs valeurs critiques du même type ; elle doit garantir de ne pas les mélanger. Par exemple,
swapFooBar :: (Foo, Bar) -> (Bar, Foo)
swapFooBar (Foo n c1, Bar s c2) = (Bar s c1, Foo n c2)
Cependant, cela peut être géré en donnant le même traitement aux structures de données qui les contiennent.