Je suis conscient que le concept d'invariants existe dans plusieurs paradigmes de programmation. Par exemple, les invariants de boucle sont pertinents en OO, en programmation fonctionnelle et procédurale.
Cependant, un type très utile trouvé dans la POO est un invariant des données d'un type particulier. C'est ce que j'appelle des "invariants basés sur le type" dans le titre. Par exemple, un Fraction
type peut avoir un numerator
et denominator
, avec l'invariant que leur pgcd est toujours 1 (c'est-à-dire que la fraction est sous une forme réduite). Je ne peux garantir cela qu'en ayant une sorte d'encapsulation du type, sans laisser ses données être définies librement. En retour, je n'ai jamais à vérifier si elle est réduite, je peux donc simplifier les algorithmes comme les contrôles d'égalité.
D'un autre côté, si je déclare simplement un Fraction
type sans fournir cette garantie par encapsulation, je ne peux pas écrire en toute sécurité des fonctions sur ce type qui supposent que la fraction est réduite, car à l'avenir quelqu'un d'autre pourrait venir et ajouter un moyen de mettre la main sur une fraction non réduite.
Généralement, l'absence de ce type d'invariant peut conduire à:
- Algorithmes plus complexes car les conditions préalables doivent être vérifiées / assurées à plusieurs endroits
- Violations SÈCHES car ces conditions préalables répétées représentent la même connaissance sous-jacente (que l'invariant doit être vrai)
- Devoir appliquer des conditions préalables par des échecs d'exécution plutôt que des garanties au moment de la compilation
Donc ma question est quelle est la réponse de programmation fonctionnelle à ce genre d'invariant. Existe-t-il une manière fonctionnelle et idiomatique de réaliser plus ou moins la même chose? Ou y a-t-il un aspect de la programmation fonctionnelle qui rend les avantages moins pertinents?
la source
PrimeNumber
classe. Il serait trop coûteux d'effectuer plusieurs vérifications redondantes de primalité pour chaque opération, mais ce n'est pas une sorte de test qui peut être effectué au moment de la compilation. (Beaucoup d'opérations que vous aimeriez effectuer sur des nombres premiers, par exemple la multiplication, ne forment pas une fermeture , c'est-à-dire que les résultats ne sont probablement pas garantis premiers. (Publication en tant que commentaires car je ne connais pas moi-même la programmation fonctionnelle.)Réponses:
Certains langages fonctionnels tels que OCaml ont des mécanismes intégrés pour implémenter des types de données abstraits, imposant ainsi certains invariants . Les langues qui ne disposent pas de tels mécanismes reposent sur le fait que l'utilisateur «ne regarde pas sous le tapis» pour appliquer les invariants.
Types de données abstraits dans OCaml
Dans OCaml, les modules sont utilisés pour structurer un programme. Un module a une implémentation et une signature , cette dernière étant une sorte de résumé des valeurs et des types définis dans le module, tandis que la première fournit les définitions réelles. Cela peut être vaguement comparé au diptyque
.c/.h
familier aux programmeurs C.À titre d'exemple, nous pouvons implémenter le
Fraction
module comme ceci:Cette définition peut maintenant être utilisée comme ceci:
N'importe qui peut produire directement des valeurs de la fraction de type, en contournant le filet de sécurité intégré
Fraction.make
:Pour éviter cela, il est possible de masquer la définition concrète du type
Fraction.t
comme ça:La seule façon de créer un
AbstractFraction.t
est d'utiliser laAbstractFraction.make
fonction.Types de données abstraits dans le schéma
Le langage Scheme n'a pas le même mécanisme de types de données abstraits qu'OCaml. Il repose sur l'utilisateur «ne regardant pas sous le tapis» pour réaliser l'encapsulation.
Dans Scheme, il est habituel de définir des prédicats comme la
fraction?
reconnaissance de valeurs donnant la possibilité de valider l'entrée. D'après mon expérience, l'usage dominant est de laisser l'utilisateur valider son entrée, s'il falsifie une valeur, plutôt que de valider l'entrée dans chaque appel de bibliothèque.Il existe cependant plusieurs stratégies pour appliquer l'abstraction des valeurs renvoyées, comme renvoyer une fermeture qui donne la valeur lorsqu'elle est appliquée ou renvoyer une référence à une valeur dans un pool géré par la bibliothèque - mais je n'en ai jamais vu en pratique.
la source
L'encapsulation n'est pas une fonctionnalité fournie avec la POO. Tout langage qui prend en charge une modularisation appropriée l'a.
Voici à peu près comment vous le faites à Haskell:
Maintenant, pour créer un Rational, vous utilisez la fonction ratio, qui applique l'invariant. Parce que les données sont immuables, vous ne pouvez plus violer l'invariant par la suite.
Cela vous coûte cependant quelque chose: il n'est plus possible pour l'utilisateur d'utiliser la même déclaration de déconstruction que l'utilisation du dénominateur et du numérateur.
la source
Vous procédez de la même manière: créez un constructeur qui applique la contrainte et acceptez d'utiliser ce constructeur chaque fois que vous créez une nouvelle valeur.
Mais Karl, dans OOP, vous n'êtes pas obligé d' accepter d'utiliser le constructeur. Oh vraiment?
En fait, les opportunités pour ce type d'abus sont moins nombreuses en PF. Vous devez mettre le constructeur en dernier, en raison de l'immuabilité. Je souhaite que les gens cessent de penser à l'encapsulation comme une sorte de protection contre des collègues incompétents, ou comme évitant la nécessité de communiquer des contraintes. Ça ne fait pas ça. Cela limite simplement les endroits que vous devez vérifier. Les bons programmeurs FP utilisent également l'encapsulation. Il se présente simplement sous la forme de la communication de quelques fonctions préférées pour effectuer certains types de modifications.
la source