web-dev-qa-db-fra.com

Tapez la vérification et les types récursifs (écrire le combinateur Y dans HASKELL / OCAML)

Lors de l'explication de la combinaison Y dans le contexte de HASKELL, il est généralement noté que la mise en œuvre directe ne choisit pas l'enregistrement de HASKELL en raison de son type récursif.

Par exemple, de Rosettacode :

The obvious definition of the Y Combinator in Haskell canot be used
because it contains an infinite recursive type (a = a -> b). Defining
a data type (Mu) allows this recursion to be broken.

newtype Mu a = Roll { unroll :: Mu a -> a }

fix :: (a -> a) -> a
fix = \f -> (\x -> f (unroll x x)) $ Roll (\x -> f (unroll x x))

Et en effet, la définition "évidente" ne tape pas la vérification:

λ> let fix f g = (\x -> \a -> f (x x) a) (\x -> \a -> f (x x) a) g

<interactive>:10:33:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    Expected type: t2 -> t0 -> t1
      Actual type: (t2 -> t0 -> t1) -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a

<interactive>:10:57:
    Occurs check: cannot construct the infinite type:
      t2 = t2 -> t0 -> t1
    In the first argument of `x', namely `x'
    In the first argument of `f', namely `(x x)'
    In the expression: f (x x) a
(0.01 secs, 1033328 bytes)

La même limitation existe dans OCAML:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
Error: This expression has type 'a -> 'b but an expression was expected of type 'a                                    
       The type variable 'a occurs inside 'a -> 'b

Cependant, dans OCAML, on peut permettre des types récursifs en passant dans le commutateur -rectypes:

   -rectypes
          Allow  arbitrary  recursive  types  during type-checking.  By default, only recursive
          types where the recursion goes through an object type are supported.

En utilisant -rectypes, tout fonctionne:

utop # let fix f g = (fun x a -> f (x x) a) (fun x a -> f (x x) a) g;;
val fix : (('a -> 'b) -> 'a -> 'b) -> 'a -> 'b = <fun>
utop # let fact_improver partial n = if n = 0 then 1 else n*partial (n-1);;
val fact_improver : (int -> int) -> int -> int = <fun>
utop # (fix fact_improver) 5;;
- : int = 120

Étant curieux des systèmes de type et de type inférence, cela soulève quelques questions que je ne suis toujours pas capable de répondre.

  • Tout d'abord, comment le vérificateur de type propose-t-il le type t2 = t2 -> t0 -> t1? Après avoir marché avec ce type, je suppose que le problème est que le type (t2) se réfère à celui du côté droit?
  • Deuxièmement, et peut-être la plus intéressante, quelle est la raison des systèmes de type HASKELL/OCAML à interdire cela? Je suppose que là est une bonne raison puisque OCAML ne l'autorisera pas par défaut même si elle peut Traiter avec des types récursifs si vous émettez le -rectypes changer.

Si ce sont vraiment de gros sujets, j'apprécierais les indicateurs à la littérature pertinente.

21
beta

Premièrement, l'erreur de GHC,

GHC tente d'unifier quelques contraintes avec x, d'abord, nous l'utilisons comme une fonction afin

x :: a -> b

Ensuite, nous l'utilisons comme valeur pour cette fonction

x :: a

Et enfin, nous l'offrons avec l'expression d'argument d'origine afin

x :: (a -> b) -> c -> d

Maintenant x x Devient une tentative d'unifier t2 -> t1 -> t0 Cependant, nous ne pouvons pas l'unifier cela puisqu'il nécessiterait unification t2, Le premier argument de x, avec x. D'où notre message d'erreur.

Ensuite, pourquoi pas les types récursifs généraux. Eh bien, le premier point à noter est la différence entre Equi et ISO Types récursifs,

  • equi-récursif est ce que vous attendez mu X . Type est exactement équivalent à l'expansion ou à la pliage arbitrairement.
  • les types iso-récursifs fournissent une paire d'opérateurs, fold et unfold qui pliez et déplacez les définitions récursives des types.

Maintenant, les types Equi-récursifs sont idéaux, mais sont absurdes difficiles à obtenir directement dans des types de types complexes. Il peut réellement faire une vérification de type indéchérable. Je ne connais pas tous les détails du système de type d'OCAML, mais les types entièrement équracursifs de HASKELL peuvent causer une boucle de type TYPECKER qui tente d'essayer d'unifier les types, par défaut, HASKELL s'assure que la vérification de type se termine. De plus, à Haskell, les synonymes de type Synonymes sont stupides, les types de récursifs les plus utiles seraient définis comme type T = T -> () _, mais sont affinés presque immédiatement à Haskell, mais vous ne pouvez pas entrer en ligne de type récursif, c'est infini! Par conséquent, les types récursifs dans HASKELL exigeraient une énorme refonte de la manière dont les synonymes sont traités, probablement ne vaut probablement pas l'effort de mettre comme une extension de langue.

Les types iso-récursifs sont un peu douloureux à utiliser, vous devez plus ou moins expliquer explicitement le vérificateur de type comment plier et déplier vos types, rendre vos programmes plus complexes à lire et à écrire.

Cependant, cela ressemble beaucoup à ce que vous faites avec votre type Mu. Roll est plié et unroll est déplié. Donc, en fait, nous avons des types iso-récursifs cuits au four. Cependant, les types Equi-récursif ne sont que trop complexes, des systèmes tels que OCAML et HASKELL vous forcent à passer des récidives à travers les points de fixation de type.

Maintenant, si cela vous intéresse, je recommanderais des types et des langages de programmation. Ma copie est assise ouverte sur mes genoux car j'écris ceci pour m'assurer que j'ai la bonne terminologie :)

16
Daniel Gratzer

À Ocaml, vous devez passer -rectypes comme paramètre sur le compilateur (ou entrez #rectypes;; dans le nombril). À titre approfondi, cela fera désactiver "Chèque survient" lors de l'unification. La situation The type variable 'a occurs inside 'a -> 'b ne sera plus un problème. Le système de type sera toujours "correct" (son, etc.), les arbres infinis qui surviennent comme des types sont parfois appelés "arbres rationnels". Le système de type devient plus faible, c'est-à-dire qu'il devient impossible de détecter certaines erreurs de programmeur.

Voir mon Lecture sur Lambda-Calculus (à partir de la diapositive 27) Pour plus d'informations sur les opérateurs de fixation avec des exemples dans OCAML.

2
lukstafi