Tout d'abord, jetez un coup d'œil à la "correspondance de Curry Howard" qui stipule que les types d'un programme informatique correspondent aux formules de la logique intuitionniste. La logique intuitionniste est tout comme la logique "régulière" que vous avez apprise à l'école, mais sans la loi du milieu exclu ou la double élimination par négation :
Les lois de la logique
Vous êtes sur la bonne voie avec les lois de DeMorgan, mais nous allons d'abord les utiliser pour en dériver de nouvelles. La version pertinente des lois de DeMorgan est la suivante :
- ∀x. P(x) = ¬∃x. ¬P(x)
- ∃x. P(x) = ¬∀x. ¬P(x)
Nous pouvons dériver (∀x. P ⇒ Q(x)) = P ⇒ (∀x. Q(x)) :
- (∀x. P ⇒ Q(x))
- (∀x. ¬P ∨ Q(x))
- ¬P ∨ (∀x. Q(x))
- P ⇒ (∀x. Q)
Et (∀x. Q(x) ⇒ P) = (∃x. Q(x)) ⇒ P (celle-ci est utilisée ci-dessous) :
- (∀x. Q(x) ⇒ P)
- (∀x. ¬Q(x) ∨ P)
- (¬¬∀x. ¬Q(x)) ∨ P
- (¬∃x. Q(x)) ∨ P
- (∃x. Q(x)) ⇒ P
Notez que ces lois sont également valables en logique intuitionniste. Les deux lois que nous avons dérivées sont citées dans l'article ci-dessous.
Types simples
Les types les plus simples sont faciles à utiliser. Par exemple :
data T = Con Int | Nil
Les constructeurs et les accesseurs ont les signatures de type suivantes :
Con :: Int -> T
Nil :: T
unCon :: T -> Int
unCon (Con x) = x
Constructeurs de type
Abordons maintenant les constructeurs de types. Prenons la définition de données suivante :
data T a = Con a | Nil
Cela crée deux constructeurs,
Con :: a -> T a
Nil :: T a
Bien sûr, en Haskell, les variables de type sont implicitement quantifiées de manière universelle, donc elles le sont vraiment :
Con :: ∀a. a -> T a
Nil :: ∀a. T a
Et l'accesseur est tout aussi simple :
unCon :: ∀a. T a -> a
unCon (Con x) = x
Types quantifiés
Ajoutons le quantificateur existentiel, ∃, à notre type original (le premier, sans le constructeur de type). Plutôt que de l'introduire dans la définition du type, qui ne ressemble pas à de la logique, introduisons-le dans les définitions du constructeur / accesseur, qui ressemblent à de la logique. Nous corrigerons la définition des données plus tard pour qu'elle corresponde.
Au lieu de Int
nous allons maintenant utiliser ∃x. t
. Ici, t
est une sorte d'expression de type.
Con :: (∃x. t) -> T
unCon :: T -> (∃x. t)
En se basant sur les règles de la logique (la deuxième règle ci-dessus), nous pouvons réécrire le type de Con
à :
Con :: ∀x. t -> T
Lorsque nous avons déplacé le quantificateur existentiel vers l'extérieur (forme prenex), il s'est transformé en un quantificateur universel.
Les éléments suivants sont donc théoriquement équivalents :
data T = Con (exists x. t) | Nil
data T = forall x. Con t | Nil
Sauf qu'il n'y a pas de syntaxe pour exists
en Haskell.
En logique non intuitionniste, il est permis de dériver ce qui suit à partir du type de unCon
:
unCon :: ∃ T -> t -- invalid!
La raison pour laquelle cela n'est pas valable est qu'une telle transformation n'est pas autorisée en logique intuitionniste. Il est donc impossible d'écrire le type pour unCon
sans un exists
et il est impossible de mettre la signature de type sous forme de prenex. Il est difficile de faire un vérificateur de type dont la fin est garantie dans de telles conditions, c'est pourquoi Haskell ne supporte pas les quantificateurs existentiels arbitraires.
Sources
"First-class Polymorphism with Type Inference", Mark P. Jones, Proceedings of the 24th ACM SIGPLAN-SIGACT symposium on Principles of programming languages ( web )