¿Cuál es una explicación breve pero completa de un sistema de tipo puro / dependiente?

32

Si algo es simple, entonces debería ser completamente explicable con unas pocas palabras. Esto se puede hacer para el cálculo λ:

El cálculo λ es una gramática sintáctica (básicamente, una estructura) con una regla de reducción (lo que significa que un procedimiento de búsqueda / reemplazo se aplica repetidamente a cada aparición de un patrón específico hasta que no exista dicho patrón).

Gramática:

Term = (Term Term) | (λ Var . Term) | Var

Regla de reducción:

((λ var body) term) -> SUBS(body,var,term)
    where `SUBS` replaces all occurrences of `var`
    by `term` in `body`, avoiding name capture.

Ejemplos:

(λ a . a)                             -> (λ a a)
((λ a . (λ b . (b a))) (λ x . x))     -> (λ b . (b (λ x x)))
((λ a . (a a)) (λ x . x))             -> (λ x . x)
((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x)))
((λ x . (x x)) (λ x . (x x)))         -> never halts

Si bien es algo informal, se podría argumentar que esto es lo suficientemente informativo para que un humano normal entienda el cálculo λ en su conjunto, y requiere 22 líneas de reducción. Estoy tratando de entender los sistemas de tipos puros / dependientes como los utilizados por Idris / Agda y proyectos similares, pero la explicación más breve que pude encontrar fue simplemente fácil : un gran trabajo, pero parece asumir muchos conocimientos previos (Haskell, inductivo definiciones) que no tengo. Creo que algo más breve y menos rico podría eliminar algunas de esas barreras. Así,

¿Es posible dar una explicación breve y completa de los sistemas de tipo puro / dependiente, en el mismo formato que presenté el cálculo λ anterior?

MaiaVictor
fuente
44
Las reglas de Pure Type Systems son muy breves. Simply Easy se trata de implementar tipos dependientes.
2
Entonces, ¿no es "hostil" en el sentido de ofensivo, pero en el sentido de que crees que estoy exigiendo mucho por no mostrar suficiente esfuerzo para encontrar la respuesta por mí mismo? Si ese es el caso, estoy de acuerdo en que esta pregunta puede ser muy exigente, así que tal vez sea simplemente mala. Pero también hay mucho esfuerzo detrás de esto, ¿crees que debería editar en mis intentos?
MaiaVictor
3
También me ofende, en nombre de mis coautores que escribieron el texto de "Una implementación tutorial de un cálculo Lambda de tipo dependiente", que reemplazó a "Simplemente fácil" como título provisional. Escribí el núcleo del código, que es un comprobador de tipos en <100 líneas de Haskell.
2
Entonces ciertamente me expresé mal. Me encanta el artículo "Simplemente fácil" y lo leo en todos los descansos desde hace unos días: es lo único que me dio una sensación parcial en el mundo. Estoy empezando a entender el tema (y creo que lo intenté). . Pero sí creo que está dirigido a un público con más conocimiento que el que tengo, y esa podría ser la razón por la que todavía tengo problemas para obtener parte de él. Nada que ver con la calidad del papel, pero mis propias limitaciones.
MaiaVictor
1
@pigworker y el código es mi parte favorita, exactamente porque (en relación con la explicación en inglés) es una explicación mucho más corta, pero completa, como pregunté aquí. ¿Por casualidad tienes una copia del código que puedo descargar?
MaiaVictor

Respuestas:

7

Renuncia

Esto es muy informal, como lo solicitó.

La gramática

En un lenguaje mecanografiado de forma dependiente, tenemos una carpeta en el nivel de tipo, así como en el nivel de valor:

Term = * | (∀ (Var : Term). Term) | (Term Term) | (λ Var. Term) | Var

El término bien escrito es un término con tipo adjunto, escribiremos t ∈ σo

σ
t

para indicar que el término ttiene tipo σ.

Reglas de mecanografía

En aras de la simplicidad es necesario que en λ v. t ∈ ∀ (v : σ). τtanto λy se unen la misma variable ( ven este caso).

Reglas:

t ∈ σ is well-formed if σ ∈ * and t is in normal form (0)

*            ∈ *                                                 (1)
∀ (v : σ). τ ∈ *             -: σ ∈ *, τ ∈ *                     (2)
λ v. t       ∈ ∀ (v : σ). τ  -: t ∈ τ                            (3)
f x          ∈ SUBS(τ, v, x) -: f ∈ ∀ (v : σ). τ, x ∈ σ          (4)
v            ∈ σ             -: v was introduced by ∀ (v : σ). τ (5)

Por lo tanto, *es "el tipo de todos los tipos" (1), los tipos de formas de los tipos (2), las abstracciones lambda tienen tipos pi (3) y si vse introducen por ∀ (v : σ). τ, entonces vtienen el tipo σ(5).

"en forma normal" significa que realizamos tantas reducciones como sea posible utilizando la regla de reducción:

"La" regla de reducción

(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ) ~> SUBS(b, v, t) ∈ SUBS(τ, v, t)
    where `SUBS` replaces all occurrences of `v`
    by `t` in `τ` and `b`, avoiding name capture.

O en sintaxis bidimensional donde

σ
t

significa t ∈ σ:

(∀ (v : σ). τ) σ    SUBS(τ, v, t)
                 ~>
(λ  v     . b) t    SUBS(b, v, t)

Solo es posible aplicar una abstracción lambda a un término cuando el término tiene el mismo tipo que la variable en el cuantificador general asociado. Luego reducimos tanto la abstracción lambda como el cuantificador total de la misma manera que en el cálculo lambda puro anterior. Después de restar la parte del nivel de valor, obtenemos la regla de escritura (4).

Un ejemplo

Aquí está el operador de la aplicación de función:

∀ (A : *) (B : A -> *) (f : ∀ (y : A). B y) (x : A). B x
λ  A       B            f                    x     . f x

(abreviamos ∀ (x : σ). τa σ -> τsi τno se menciona x)

fdevuelve B ypara cualquier proporcionado yde tipo A. Aplicamos fa x, que es del tipo correcto A, y el sustituto ypara xel después ., por lo tanto f x ∈ SUBS(B y, y, x)~> f x ∈ B x.

Vamos a abreviar ahora el operador de aplicación de función como appy aplicarlo a sí mismo:

∀ (A : *) (B : A -> *). ?
λ  A       B          . app ? ? (app A B)

Coloco los ?términos que necesitamos proporcionar. Primero introducimos e instanciamos explícitamente Ay B:

∀ (f : ∀ (y : A). B y) (x : A). B x
app A B

Ahora necesitamos unificar lo que tenemos

∀ (f : ∀ (y : A). B y) (x : A). B x

que es lo mismo que

(∀ (y : A). B y) -> ∀ (x : A). B x

y lo que app ? ?recibe

∀ (x : A'). B' x

Esto resulta en

A' ~ ∀ (y : A). B y
B' ~ λ _. ∀ (x : A). B x -- B' ignores its argument

(ver también ¿Qué es la predicatividad? )

Nuestra expresión (después de un cambio de nombre) se convierte en

∀ (A : *) (B : A -> *). ?
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

Dado que para cualquiera A, By f(donde f ∈ ∀ (y : A). B y)

∀ (y : A). B y
app A B f

podemos instanciar Ay Bobtener (para cualquiera fcon el tipo apropiado)

∀ (y : ∀ (x : A). B x). ∀ (x : A). B x
app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) f

y la firma de tipo es equivalente a (∀ (x : A). B x) -> ∀ (x : A). B x.

Toda la expresión es

∀ (A : *) (B : A -> *). (∀ (x : A). B x) -> ∀ (x : A). B x
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

Es decir

∀ (A : *) (B : A -> *) (f : ∀ (x : A). B x) (x : A). B x
λ  A       B            f                    x     .
    app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B) f x

que después de todas las reducciones en el nivel de valor devuelve lo mismo app.

Así, mientras que requiere sólo unos pasos en el cálculo lambda pura para obtener appa partir app app, en un entorno mecanografiada (y sobre todo una manera dependiente mecanografiadas) también hay que preocuparse por la unificación y las cosas se vuelven más complejas, incluso con cierta comodidad inconsitent ( * ∈ *).

Comprobación de tipo

  • Si tes *entonces t ∈ *por (1)
  • Si tes ∀ (x : σ) τ, σ ∈? *, τ ∈? *(véase la nota sobre ∈?abajo) y luego t ∈ *por (2)
  • Si tes f x, f ∈ ∀ (v : σ) τpara algunos σy τ, x ∈? σentonces t ∈ SUBS(τ, v, x)por (4)
  • Si tes una variable v, vfue introducido por ∀ (v : σ). τentonces t ∈ σpor (5)

Todas estas son reglas de inferencia, pero no podemos hacer lo mismo para lambdas (la inferencia de tipos es indecidible para los tipos dependientes). Entonces, para lambdas, verificamos ( t ∈? σ) en lugar de inferir:

  • Si tes λ v. by comprobado ∀ (v : σ) τ, b ∈? τentoncest ∈ ∀ (v : σ) τ
  • Si tes otra cosa y se compara σ, deduzca el tipo de tuso de la función anterior y verifique si esσ

La verificación de igualdad de tipos requiere que estén en formas normales, por lo que para decidir si ttiene tipo σ, primero verificamos que σtenga tipo *. Si es así, entonces σes normalizable (paradoja del módulo Girard) y se normaliza (por lo tanto, σestá bien formado por (0)). SUBStambién normaliza expresiones para preservar (0).

Esto se llama verificación de tipo bidireccional. Con él no necesitamos anotar cada lambda con un tipo: si se conoce f xel tipo de f, entonces xse verifica con el tipo de argumento que frecibe en lugar de inferirse y compararse para la igualdad (que también es menos eficiente). Pero si fes un lambda, requiere un tipo de anotación explícita (anotaciones se omiten en la gramática y en todas partes, puede agregar Ann Term Termo λ' (σ : Term) (v : Var)a los constructores).

Además, eche un vistazo al más simple y fácil. entrada en el blog.

usuario3237465
fuente
1
Secundario "Más simple, más fácil".
La primera regla de reducción para todos parece extraña. A diferencia de las lambdas, los foralls no deben aplicarse de una manera bien tipada (¿verdad?).
@chi, no entiendo lo que estás diciendo. Quizás mi notación es mala: la regla de reducción dice (λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)~> SUBS(b, v, t) ∈ SUBS(τ, v, t).
user3237465
1
La notación me parece engañosa. Parece como si tuvieras dos reglas: una para lo absurdo (∀ (v : σ). τ) t ~> ...y otra para lo significativo (λ v. b) t ~> .... Quitaría el primero y lo convertiría en un comentario a continuación.
1
La regla (1) contiene su conclusión como premisa. Puede comparar la simplicidad de su sistema con la versión bidireccional solo una vez que tenga un sistema que funcione. Puede decir que mantiene todo normalizado, pero sus reglas no lo hacen.
24

Vamos a intentarlo. No me molestaré en la paradoja de Girard, porque distrae de las ideas centrales. Tendré que presentar algunos mecanismos de presentación sobre juicios y derivaciones y demás.

Gramática

Término :: = (Elim) | * | (Var: Término) → Término | λVar↦Term

Elim :: = Término: Término | Var | Término Elim

La gramática tiene dos formas mutuamente definidas, "términos", que son la noción general de cosa (los tipos son cosas, los valores son cosas), que incluyen * (el tipo de tipos), tipos de funciones dependientes y abstracciones lambda, pero también incrustación " eliminaciones "(es decir," usos "en lugar de" construcciones "), que son aplicaciones anidadas donde la cosa en última instancia en la posición de función es una variable o un término anotado con su tipo.

Reglas de reducción

(λy↦t: (x: S) → T) s ↝ t [s: S / y]: T [s: S / x]

(t: T) ↝ t

La operación de sustitución t [e / x] reemplaza cada aparición de la variable x con la eliminación e, evitando la captura de nombres. Para formar una aplicación que pueda reducir, un término lambda debe ser anotado por su tipo para hacer una eliminación . La anotación de tipo le da a la abstracción lambda una especie de "reactividad", permitiendo que la aplicación continúe. Una vez que llegamos al punto en que ya no está ocurriendo más aplicación y el t: T activo se está volviendo a insertar en el sintaxis del término, podemos eliminar la anotación de tipo.

Extendamos la relación de reducción ↝ por cierre estructural: las reglas se aplican en cualquier lugar dentro de los términos y eliminaciones en los que puede encontrar algo que coincida con el lado izquierdo. Escriba ↝ * para el cierre reflexivo-transitivo (0 o más pasos) de ↝. El sistema de reducción resultante es confluente en este sentido:

Si s ↝ * p y s ↝ * q, entonces existe alguna r tal que p ↝ * r y q ↝ * r.

Contextos

Contexto :: = | Contexto, Var: Término

Los contextos son secuencias que asignan tipos a variables, que crecen a la derecha, que consideramos como el fin "local", que nos informa sobre las variables enlazadas más recientemente. Una propiedad importante de los contextos es que siempre es posible elegir una variable que ya no se utiliza en el contexto. Mantenemos la invariante de que las variables de los tipos asignados en el contexto son distintas.

Juicios

Juicio :: = Contexto ⊢ Término tiene Término | Contexto ⊢ Elim es Término

Esa es la gramática de los juicios, pero ¿cómo leerlos? Para empezar, ⊢ es el símbolo tradicional del "torniquete", que separa los supuestos de las conclusiones: puede leerlo informalmente como "dice".

G ⊢ T tiene t

significa que dado el contexto G, el tipo T admite el término t;

G ⊢ e es S

significa que dado el contexto G, la eliminación e recibe el tipo S.

Los juicios tienen una estructura interesante: cero o más entradas , uno o más sujetos , cero o más salidas .

INPUTS                   SUBJECT        OUTPUTS
Context |- Term   has    Term
Context |-               Elim      is   Term

Es decir, debemos proponer los tipos de términos por adelantado y simplemente verificarlos , pero sintetizamos los tipos de eliminaciones.

Reglas de mecanografía

Los presento en un vago estilo Prolog, escribiendo J -: P1; ... Pn para indicar que el juicio J se cumple si las premisas P1 a Pn también se cumplen. Una premisa será otro juicio o un reclamo sobre la reducción.

Condiciones

G ⊢ T tiene t -: T ↝ R; G ⊢ R tiene t

G has * tiene *

G ⊢ * tiene (x: S) → T -: G ⊢ * tiene S; G, z: S! - * tiene T [z / x]

G ⊢ (x: S) → T tiene λy↦t -: G, z: S ⊢ T [z / x] tiene t [z / y]

G ⊢ T tiene (e) -: G ⊢ e es T

Eliminaciones

G ⊢ e es R -: G ⊢ e es S; S ↝ R

G, x: S, G '⊢ x es S

G ⊢ fs es T [s: S / x] -: G ⊢ f es (x: S) → T; G ⊢ S tiene s

¡Y eso es!

Solo dos reglas no están dirigidas por la sintaxis: la regla que dice "puede reducir un tipo antes de usarlo para verificar un término" y la regla que dice "puede reducir un tipo después de sintetizarlo a partir de una eliminación". Una estrategia viable es reducir los tipos hasta que haya expuesto el constructor superior.

Este sistema no se está normalizando fuertemente (debido a la paradoja de Girard, que es una paradoja de auto-referencia de mentiroso), pero se puede normalizar fuertemente dividiendo * en "niveles de universo" donde cualquier valor que involucre tipos en niveles más bajos tener tipos en niveles más altos, evitando la autorreferencia.

Sin embargo, este sistema tiene la propiedad de preservación de tipo, en este sentido.

Si G ⊢ T tiene t y G ↝ * D y T ↝ * R y t ↝ r, entonces D ⊢ R tiene r.

Si G ⊢ e es S y G ↝ * D y e ↝ f, entonces existe R tal que S ↝ * R y D ⊢ f es R.

Los contextos pueden computar permitiendo que computen los términos que contienen. Es decir, si un juicio es válido ahora, puede calcular sus entradas tanto como desee y su tema en un solo paso, y luego será posible calcular sus salidas de alguna manera para asegurarse de que el juicio resultante permanezca válido. La prueba es una inducción simple en derivaciones de escritura, dada la confluencia de -> *.

Por supuesto, he presentado solo el núcleo funcional aquí, pero las extensiones pueden ser bastante modulares. Aquí hay pares.

Término :: = ... | (x: S) * T | S t

Elim :: = ... | e.head | e.tail

(s, t: (x: S) * T) .head ↝ s: S

(s, t: (x: S) * T) .tail ↝ t: T [s: S / x]

G ⊢ * tiene (x: S) * T -: G ⊢ * tiene S; G, z: S ⊢ * tiene T [z / x]

G ⊢ (x: S) * T tiene s, t -: G ⊢ S tiene s; G ⊢ T [s: S / x] tiene t

G ⊢ e.head es S -: G ⊢ e es (x: S) * T

G ⊢ e.tail es T [e.head / x] -: G ⊢ e es (x: S) * T

trabajador porcino
fuente
1
G, x:S, G' ⊢ x is S -: G' ⊬ x?
user3237465
1
@ user3237465 No. ¡Gracias! Fijo. (Cuando yo estaba reemplazando torniquetes arte ASCII con html torniquetes (lo que los hace invisibles en mi teléfono, lo siento si lo que está sucediendo en otras partes) que se perdió esa.)
1
Oh, pensé que solo estabas señalando el error tipográfico. La regla dice que, para cada variable en el contexto, sintetizamos el tipo que le asigna el contexto. Al presentar los contextos, dije "Mantenemos la invariante de que las variables que se asignan a los tipos en el contexto son distintas". así que no se permiten las sombras. Verá que cada vez que las reglas extienden el contexto, siempre eligen una nueva "z" que crea una instancia de las carpetas que estamos siguiendo. La sombra es anatema. Si tiene contexto x: *, x: x, el tipo de la x más local ya no está bien porque está fuera de alcance.
1
Solo quería que usted y los demás respondedores supieran que volveré a este hilo cada vez que salga del trabajo. Tengo muchas ganas de aprender esto, y por primera vez me caí como si realmente obtuviera la mayor parte. El siguiente paso será implementar y escribir algunos programas. Estoy encantado de poder vivir en una era donde la información sobre temas tan maravillosos está disponible en todo el mundo para alguien como yo, y todo gracias a genios como usted que dedican un tiempo de sus vidas a difundir ese conocimiento, por gratis, en internet. Perdón por haber formulado mal mi pregunta, ¡y gracias!
MaiaVictor
1
@ cody Sí, no hay expansión. Para ver por qué no es necesario, tenga en cuenta que las dos reglas de cálculo le permiten implementar la estrategia donde normaliza los tipos por completo antes de verificar los términos, y también normaliza los tipos inmediatamente después de sintetizarlos a partir de las eliminaciones. Entonces, en la regla donde los tipos deben coincidir, ya están normalizados, por lo tanto, son iguales en la nariz si los tipos "originales" verificados y sintetizados son convertibles. Mientras tanto, restringir las verificaciones de igualdad a ese lugar solo está bien debido a este hecho: si T es convertible a un tipo canónico, se reduce a un tipo canónico.
pigworker
8

La correspondencia de Curry-Howard dice que existe una correspondencia sistemática entre los sistemas de tipo y los sistemas de prueba en lógica. Tomando una vista centrada en el programador de esto, puede volver a emitirlo de esta manera:

  • Los sistemas de prueba lógica son lenguajes de programación.
  • Estos idiomas son de tipo estático.
  • La responsabilidad del sistema de tipos en dicho lenguaje es prohibir los programas que construirían pruebas erróneas.

Visto desde este ángulo:

  • El cálculo lambda sin tipo que usted resume no tiene un sistema de tipos significativo, por lo que un sistema de prueba construido sobre él no sería sólido.
  • El cálculo lambda de tipo simple es un lenguaje de programación que tiene todos los tipos necesarios para construir pruebas de sonido en lógica oracional ("if / then", "and", "o", "not"). Pero sus tipos no son lo suficientemente buenos como para verificar las pruebas que involucran cuantificadores ("para todas las x, ..."; "existe una x tal que ...").
  • El cálculo lambda tipificado de forma dependiente tiene tipos y reglas que admiten la lógica oracional y los cuantificadores de primer orden (cuantificación sobre valores).

Aquí están las reglas de deducción natural para la lógica de primer orden, usando un diagrama de la entrada de Wikipedia sobre deducción natural . Estas son básicamente las reglas de un cálculo lambda de tipo dependiente mínimo también.

Deducción natural de primer orden

Tenga en cuenta que las reglas tienen términos lambda en ellas. Estos pueden leerse como los programas que construyen las pruebas de las oraciones representadas por sus tipos (o más sucintamente, solo decimos que los programas son pruebas ). Reglas de reducción similares que usted puede aplicar a estos términos lambda.


¿Por qué nos importa esto? Bueno, en primer lugar, porque las pruebas pueden resultar ser una herramienta útil en la programación, y tener un lenguaje que pueda funcionar con las pruebas como objetos de primera clase abre muchos caminos. Por ejemplo, si su función tiene una condición previa, en lugar de escribirla como un comentario, puede exigir una prueba de ello como argumento.

Segundo, porque la maquinaria del sistema de tipos necesaria para manejar cuantificadores puede tener otros usos en un contexto de programación. En particular, los lenguajes de tipo dependiente manejan cuantificadores universales ("para todas las x, ...") mediante el uso de un concepto llamado tipos de funciones dependientes, una función en la que el tipo estático del resultado puede depender del valor de tiempo de ejecución del argumento.

Para dar una aplicación muy peatonal de esto, escribo código todo el tiempo que tiene que leer archivos Avro que consisten en registros con estructura uniforme, todos comparten el mismo conjunto de nombres y tipos de campo. Esto requiere que yo:

  1. Codifique la estructura de los registros en el programa como un tipo de registro.
    • Ventajas: el código es más simple y el compilador puede detectar errores en mi código
    • Desventaja: el programa está codificado para leer archivos que coinciden con el tipo de registro.
  2. Lea el esquema de los datos en tiempo de ejecución, represéntelo genéricamente como una estructura de datos y úselo para procesar registros genéricamente
    • Ventajas: mi programa no está codificado en un solo tipo de archivo
    • Desventajas: el compilador no puede detectar tantos errores.

Como puede ver en la página del tutorial de Avro Java , le muestran cómo usar la biblioteca de acuerdo con estos dos enfoques.

Con los tipos de funciones dependientes, puede tener su pastel y comerlo, a costa de un sistema de tipos más complejo. Podría escribir una función que lea un archivo Avro, extraiga el esquema y devuelva el contenido del archivo como una secuencia de registros cuyo tipo estático depende del esquema almacenado en el archivo . El compilador podría detectar errores donde, por ejemplo, intenté acceder a un campo con nombre que podría no existir en los registros de los archivos que procesará en tiempo de ejecución. Dulce, ¿eh?

sacundim
fuente
1
Construir tipos en tiempo de ejecución de la manera que mencionaste es algo realmente genial que no he pensado. ¡Muy dulce, de hecho! Gracias por la perspicaz respuesta.
MaiaVictor