Verificación de tipos y tipos recursivos (Escribir el combinador Y en Haskell / Ocaml)

21

Al explicar el combinador Y en el contexto de Haskell, generalmente se observa que la implementación sencilla no verificará en Haskell debido a su tipo recursivo.

Por ejemplo, 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))

Y, de hecho, la definición "obvia" no escribe check:

λ> 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 misma limitación existe en 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

Sin embargo, en Ocaml, uno puede permitir tipos recursivos al pasar el -rectypesinterruptor:

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

Al usar -rectypes, todo funciona:

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

Siendo curioso sobre los sistemas de tipos y la inferencia de tipos, esto plantea algunas preguntas que todavía no puedo responder.

  • Primero, ¿cómo aparece el verificador de tipos t2 = t2 -> t0 -> t1? Habiendo creado ese tipo, supongo que el problema es que el tipo ( t2) se refiere a sí mismo en el lado derecho.
  • En segundo lugar, y quizás lo más interesante, ¿cuál es la razón por la cual los sistemas de tipo Haskell / Ocaml no permiten esto? Supongo que es una buena razón, ya Ocaml también no permitirá que de forma predeterminada, incluso si se puede tratar con tipos recursivos si se les da el -rectypesinterruptor.

Si estos son temas realmente importantes, agradecería los consejos sobre literatura relevante.

beta
fuente

Respuestas:

16

Primero, el error de GHC,

GHC está intentando unificar algunas restricciones con x, primero, lo usamos como una función para

x :: a -> b

Luego lo usamos como un valor para esa función

x :: a

Y finalmente lo unificamos con la expresión de argumento original para

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

Ahora se x xconvierte en un intento de unificación t2 -> t1 -> t0, sin embargo, no podemos unificar esto ya que requeriría unificar t2, el primer argumento de x, con x. De ahí nuestro mensaje de error.

A continuación, ¿por qué no los tipos recursivos generales? Bueno, el primer punto a destacar es la diferencia entre los tipos recursivos equi e iso,

  • equi-recursivo es lo que cabría esperar mu X . Typees exactamente equivalente a expandirlo o plegarlo arbitrariamente.
  • Los tipos iso-recursivos proporcionan un par de operadores, foldy unfoldque pliegan y despliegan las definiciones recursivas de tipos.

Ahora los tipos equi-recursivos suenan ideales, pero son absurdamente difíciles de acertar en sistemas de tipos complejos. En realidad, puede hacer que la verificación de tipo sea indecidible. No estoy familiarizado con todos los detalles del sistema de tipos de OCaml, pero los tipos totalmente equivalentes en Haskell pueden hacer que el typechecker realice un bucle arbitrario tratando de unificar tipos, por defecto, Haskell se asegura de que la verificación de tipos finalice. Además, en Haskell, los sinónimos de tipo son tontos, los tipos recursivos más útiles se definirían como type T = T -> (), sin embargo, se insertan casi inmediatamente en Haskell, pero no se puede insertar un tipo recursivo, ¡es infinito! Por lo tanto, los tipos recursivos en Haskell exigirían una gran revisión de cómo se manejan los sinónimos, probablemente no vale la pena poner el esfuerzo incluso como una extensión de idioma.

Los tipos iso-recursivos son un poco difíciles de usar, más o menos tienes que decirle explícitamente al verificador de tipos cómo doblar y desplegar tus tipos, lo que hace que tus programas sean más complejos de leer y escribir.

Sin embargo, esto es muy similar a lo que estás haciendo con tu Mutipo. Rollse pliega y unrollse despliega. Entonces, en realidad, tenemos tipos iso-recursivos incorporados. Sin embargo, los tipos equ-recursivos son demasiado complejos, por lo que sistemas como OCaml y Haskell lo obligan a pasar recurrencias a través de puntos de fijación de nivel de tipo.

Ahora, si esto le interesa, recomendaría Tipos y lenguajes de programación. Mi copia está abierta en mi regazo mientras escribo esto para asegurarme de que tengo la terminología correcta :)

Daniel Gratzer
fuente
En particular, el capítulo 21 proporciona una buena intuición para la inducción, la coinducción y los tipos recursivos
Daniel Gratzer
¡Gracias! Esto es realmente fascinante. Actualmente estoy leyendo TAPL, y me alegra saber que esto se tratará más adelante en el libro.
beta
@beta Sí, TAPL y su hermano mayor, Temas avanzados en tipos y lenguajes de programación, son recursos maravillosos.
Daniel Gratzer
2

En OCaml, debe pasar -rectypescomo parámetro al compilador (o ingresar #rectypes;;en el nivel superior). Hablando en términos generales, esto desactivará la "verificación de ocurrencia" durante la unificación. La situación The type variable 'a occurs inside 'a -> 'bya no será un problema. El sistema de tipos seguirá siendo "correcto" (sonido, etc.), los árboles infinitos que surgen como tipos a veces se denominan "árboles racionales". El sistema de tipos se debilita, es decir, se hace imposible detectar algunos errores del programador.

Consulte mi conferencia sobre cálculo lambda (comenzando en la diapositiva 27) para obtener más información sobre operadores de punto fijo con ejemplos en OCaml.

lukstafi
fuente