Arend: lenguaje de tipo dependiente basado en HoTT (parte 2)

En la primera parte del artículo sobre el lenguaje Arend, examinamos los tipos inductivos más simples, funciones recursivas, clases y conjuntos.

2. Ordenar listas en Arend


2.1 Listas ordenadas en Arend


Definimos el tipo de listas ordenadas como un par que consiste en una lista y prueba de su orden. Como ya dijimos, en Arend, los pares dependientes se definen usando la palabra clave \Sigma . Damos la definición del tipo ordenado a través de la comparación con la muestra, inspirada en la definición del artículo ya mencionado sobre listas ordenadas.

 \func SortedList (O : LinearOrder.Dec) => \Sigma (l : List O) (Sorted l) \data Sorted {A : LinearOrder.Dec} (xs : List A) \elim xs | nil => nilSorted | :-: x nil => singletonSorted | :-: x1 (:-: x2 xs) => consSorted ((x1 = x2) || (x1 < x2)) (Sorted (x2 :-: xs)) 

Nota: Arend pudo inferir automáticamente que el tipo Sorted está contenido en el universo \Prop . Esto sucedió porque los tres patrones en la definición Sorted son mutuamente excluyentes, y el constructor consSorted tiene dos parámetros, los cuales pertenecen a \Prop .
Probemos alguna propiedad obvia del predicado ordenado, digamos que la cola de una lista ordenada es en sí misma una lista ordenada (esta propiedad nos será útil en el futuro).

 \func tail-sorted {O : LinearOrder.Dec} (x : O) (xs : List O) (A : Sorted (x :-: xs)) : Sorted xs \elim xs, A | nil, _ => nilSorted | :-: _ _, consSorted _ xs-sorted => xs-sorted 

En la tail-sorted utilizamos la coincidencia de patrones en la lista xs y el predicado tail-sorted al mismo tiempo, además, utilizamos el carácter de omisión "_", que puede sustituirse por variables no utilizadas.

Uno puede preguntar si es posible en Arend probar la propiedad de las listas ordenadas, mencionadas en la sección 1.3 como un ejemplo de un hecho que no puede probarse en Agda sin anotaciones de inmaterialidad. Recuerde que esta propiedad afirma que para probar la igualdad de las listas ordenadas definidas a través de pares dependientes, es suficiente verificar la igualdad de los primeros componentes de los pares.

Se argumenta que en Arend esta propiedad se obtiene fácilmente como consecuencia de la construcción inProp mencionada anteriormente y la propiedad de extensionalidad para pares SigmaExt dependientes.

 \func sorted-equality {A : LinearOrder.Dec} (l1 l2 : SortedList A) (P : l1.1 = l2.1) : l1 = l2 => SigmaPropExt Sorted l1 l2 P 

La propiedad SigmaPropExt prueba en el módulo Paths de la biblioteca estándar, muchos otros hechos del segundo capítulo del libro HoTT , incluida la propiedad de extensibilidad funcional , también se prueban allí.

El operador .n usa en Arend para acceder al proyector de tipo sigma con el número n (en nuestro caso, el tipo sigma es SortedList A , y la expresión l1.1 significa que el primer componente de este tipo es una expresión de tipo List A ).

2.2 Implementación de la propiedad "be permutation"


Ahora intentemos implementar la función de clasificación de listas en Arend. Naturalmente, no queremos tener una implementación simple del algoritmo de clasificación, sino una implementación junto con una prueba de algunas propiedades.

Claramente, este algoritmo debe tener al menos 2 propiedades:
1. El resultado del algoritmo debe ser una lista ordenada.
2. La lista resultante debe ser una permutación de la lista original.

Primero, intentemos implementar la propiedad "be permutation" de las listas en Arend. Para hacer esto, adaptamos la definición tomada de aquí para Arend.

 \truncated \data InsertSpec {A : \Set} (xs : List A) (a : A) (ys : List A) : \Prop \elim xs, ys | xs, :-: y ys => insertedHere (a = y) (xs = ys) | :-: x xs, :-: y ys => insertedThere (x = y) (InsertSpec xs a ys) \truncated \data Perm {A : \Set} (xs ys : List A) : \Prop | permInsert (xs' ys' : List A) (a : A) (Perm xs' ys') (InsertSpec xs' a xs) (InsertSpec ys' a ys) | permTrivial (xs = ys) 

El predicado InsertSpec introducido por nosotros tiene el siguiente significado intuitivo: InsertSpec xs a ys significa exactamente que la lista ys es el resultado de insertar el elemento a dentro de la lista xs (en cualquier posición). Por lo tanto, InsertSpec se puede tomar como una especificación de la función de inserción.

Está claro que el tipo de datos Perm realmente define la relación "ser permutación": el constructor permInsert exactamente que xs e ys son mutuamente permutables si se obtienen xs e ys insertando el mismo elemento a en algunas listas xs' e ys' longitudes más cortas, que ya son permutaciones entre sí.

Para nuestra definición de la propiedad "ser permutación", es fácil verificar la propiedad de simetría.

 \func Perm-symmetric {A : \Set} {xs ys : List A} (P : Perm xs ys) : Perm ys xs \elim P | permTrivial xs=ys => permTrivial (inv xs=ys) | permInsert perm-xs'-ys' xs-spec ys-spec => permInsert (Perm-symmetric perm-xs'-ys') ys-spec xs-spec 

La propiedad de transitividad también se satisface para Perm , pero su verificación es mucho más complicada. Dado que esta propiedad no juega ningún papel en la implementación de nuestro algoritmo de clasificación, lo dejamos al lector como un ejercicio.

 \func Perm-transitive {A : \Set} (xs ys zs : List A) (P1 : Perm xs ys) (P2 : Perm ys zs) : Perm xs zs => {?} 

2.3 Cambio en los niveles de homotopía en comparación con la muestra


Ahora intentemos implementar una función que inserte un elemento en una lista ordenada para que la lista resultante permanezca ordenada. Comencemos con la siguiente implementación ingenua.

 \func insert {O : LinearOrder.Dec} (xs : List O) (y : O) : List O \elim xs | nil => y :-: nil | :-: x xs' => \case LinearOrder.trichotomy xy \with {  | byLeft x=y => x :-: insert xs' y  | byRight (byLeft x<y) => x :-: insert xs' y  | byRight (byRight y<x) => y :-: x :-: xs' } 

La construcción \case permite la coincidencia con una muestra de una expresión arbitraria ( \elim se puede usar en el nivel más alto de una definición de función y solo para sus parámetros). Si le pide a Arend que verifique el tipo de insert , se mostrará el siguiente mensaje de error.

 [ERROR] Data type '||' is truncated to the universe \Prop  which does not fit in the universe of the eliminator type: List OE In: | byLeft x-leq-y => x :-: insert xs' y While processing: insert 

El problema es que en la clase LinearOrder.Dec , la LinearOrder.Dec define utilizando || , que, a su vez, se determina utilizando el truncamiento proposicional. Como ya se mencionó, para los tipos que pertenecen al universo \Prop , la coincidencia con un patrón en Arend solo se permite si el tipo de la expresión resultante es en sí misma una aserción (para la función anterior, el resultado es de tipo List OE , y este tipo es un conjunto).

¿Hay alguna forma de evitar este problema? La forma más fácil de resolver esto es cambiar la definición de la propiedad de la tricotomía. Considere la siguiente definición de tricotomía, utilizando el tipo no truncado Or lugar del truncado || :

 \func set-trichotomy {A : StrictPoset} (xy : A) => ((x = y) `Or` (x < y)) `Or` (y < x) 

¿Esta definición trichotomy en algo de la definición de trichotomy original a través de || ? ¿Por qué incluso usamos un tipo truncado proposicionalmente si solo complica nuestra vida y nos impide usar la coincidencia de patrones?

Intentemos responder la primera pregunta para empezar: para órdenes estrictas de StrictPoset diferencia entre trichotomy y trichotomy set-trichotomy no es en absoluto. Tenga en cuenta que el tipo set-trichotomy es una declaración. Este hecho se deduce del hecho de que las tres alternativas en la definición de tricotomía son mutuamente excluyentes debido a los axiomas de orden, y cada uno de los tres tipos x = y, x < y, y < x es en sí mismo una declaración ( x = y es una declaración, por lo que como en la definición de la clase BaseSet que los medios E un conjunto).

 \func set-trichotomy-isProp {A : StrictPoset} (xy : A) (l1 l2 : set-trichotomy xy): l1 = l2 \elim l1, l2 | inl (inl l1), inl (inl l2) => pmap (\lam z => inl (inl z)) (Path.inProp l1 l2) | inl (inr l1), inl (inr l2) => pmap (\lam z => inl (inr z)) (Path.inProp l1 l2) | inr l1, inr l2 => pmap inr (Path.inProp l1 l2) | inl (inl l1), inl (inr l2) => absurd (lt-eq-false l1 l2) | inl (inr l1), inl (inl l2) => absurd (lt-eq-false l2 l1) | inl (inl l1), inr l2 => absurd (lt-eq-false (inv l1) l2) | inr l1, inl (inl l2) => absurd (lt-eq-false (inv l2) l1) | inl (inr l1), inr l2 => absurd (lt-lt-false l1 l2) | inr l1, inl (inr l2) => absurd (lt-lt-false l2 l1) \where {  \func lt-eq-false {A : StrictPoset} {xy : A} (l1 : x = y) (l2 : x < y) : Empty =>    A.<-irreflexive x (transport (x <) (inv l1) l2)  \func lt-lt-false {A : StrictPoset} {xy : A} (l1 : x < y) (l2 : y < x) : Empty =>   A.<-irreflexive x (A.<-transitive _ _ _ l1 l2) } 

En la lista anterior, absurd es la designación para el principio ex falso quodlibet, que se define en el módulo Lógico . Dado que el tipo Empty no tiene constructores en la definición (ver sección 1.2), no es necesario pasar por los casos en la definición de absurd :

 \func absurd {A : \Type} (x : Empty) : A 

Dado que ahora sabemos que set-trichotomy es una declaración, podemos derivar la propiedad set-trichotomy propiedad habitual de trichotomy de órdenes decidibles. Para hacer esto, podemos usar la construcción \return \level , que le dice al temporizador Arend que en este punto la coincidencia de patrones es una operación permitida (en este caso, tenemos que demostrar que el resultado de la función set-trichotomy-property es una declaración).

 \func set-trichotomy-property {A : LinearOrder.Dec} (xy : A) : set-trichotomy xy => \case A.trichotomy xy \return \level (set-trichotomy xy) (set-trichotomy-isProp xy) \with {  | byLeft x=y => inl (inl x=y)  | byRight (byLeft x<y) => inl (inr x<y)  | byRight (byRight y<x) => inr (y<x) } 

Tratemos ahora de responder la segunda pregunta, a saber, por qué, al formular las propiedades de los objetos matemáticos, es preferible usar construcciones no ordinarias, sino truncadas proposicionalmente. Considere para esto un fragmento de la definición de órdenes lineales no lineales (las definiciones completas de Lattice y TotalOrder se pueden encontrar en el módulo LinearOrder ):

 \class TotalOrder \extends Lattice { | totality (xy : E) : x <= y || y <= x } 

Tratemos de imaginar ahora cómo TotalOrder el significado de la clase TotalOrder si TotalOrder la definición del campo de totalidad a través de la construcción Or truncada.

 \class BadTotalOrder \extends Lattice { | badTotality (xy : E) : (x <= y) `Or` (y <= x) } 

En este caso, el tipo (x <= y) `Or` (y <= x) ya no es una declaración, porque en caso de valores iguales de x e y ambas alternativas en la definición de badTotality se pueden implementar, y la elección de la rama izquierda o derecha en la prueba de badTotality absolutamente arbitraria y queda a discreción del usuario; no hay razón para preferir uno Or un constructor a otro.

Ahora está claro cuál es la diferencia entre TotalOrder y BadTotalOrder . Dos conjuntos ordenados O1 O2 : TotalOrder siempre son iguales cuando es posible demostrar la igualdad de los conjuntos O1.E, O2.E y las órdenes O1.<, O2.< Dado en ellos (esta es la propiedad deseada). Por otro lado, para O1 O2 : BadTotalOrder es BadTotalOrder probar la igualdad de O1 y O2 solo cuando, además de todos los elementos x de E O1.badTotality xx E igualdad O1.badTotality xx y O2.badTotality xx .

Por lo tanto, resulta que la clase BadTotalOrder intuitivamente debe considerarse no como un "conjunto ordenado linealmente", sino como un "conjunto ordenado linealmente junto con la elección para cada elemento x campo E rama izquierda o derecha Or en la implementación de badTotality xx ".

2.4 Algoritmo de clasificación


Ahora procedemos a implementar el algoritmo de clasificación. Tratemos de arreglar la implementación ingenua de la función de insert de la sección anterior utilizando la set-trichotomy-property probada set-trichotomy-property (en este caso, debido a la disposición más exitosa de los corchetes en la definición de set-trichotomy , hemos reducido el número de casos considerados).

 \func insert {O : LinearOrder.Dec} (xs : List O) (y : O) : List O \elim xs | nil => y :-: nil | :-: x xs' => \case set-trichotomy-property xy \with {  | inr y<x => y :-: x :-: xs'  | inl x<=y => x :-: insert xs' y } 

Ahora intentemos implementar un análogo de esta definición para las listas ordenadas. Usaremos la construcción especial \let … \in , que nos permite agregar nuevas variables locales al contexto.

 \func insert {O : LinearOrder.Dec} (xs : SortedList O) (y : O) : SortedList O \elim xs | (nil, _) => (y :-: nil, singletonSorted) | (:-: x xs', xs-sorted) => \case set-trichotomy-property xy \with {  | inr y<x => (y :-: x :-: xs', consSorted (byRight y<x) xs-sorted)  | inl x<=y => \let (result, result-sorted) => insert (xs', tail-sorted x xs' xs-sorted) y         \in (x :-: result, {?}) 

Dejamos en la prueba un fragmento incompleto (indicado por la expresión {?} ) En el lugar donde desea mostrar que la lista x :-: result ordenado. Aunque en el contexto hay evidencia del orden de la lista de result , nos queda por verificar que x no exceda el valor del primer elemento de la lista de result , que no es tan fácil de seguir desde las premisas en el contexto (para ver todas las premisas en el objetivo actual - esto es lo que llamamos rama actual de los cálculos: debe solicitar la verificación de tipo desde la función de insert ).

Resulta que la insert mucho más fácil de implementar si probamos el orden de la lista resultante en paralelo con la prueba de la especificación de insert . Cambie la firma de insert y escriba la prueba de esta especificación en los casos más simples:

 \func insert {O : LinearOrder.Dec} (xs : SortedList O) (y : O) : \Sigma (ys : SortedList O) (InsertSpec xs.1 y ys.1) \elim xs | (nil, _) => ((y :-: nil, singletonSorted), insertedHere idp idp) | (:-: x xs', xs-sorted) => \case set-trichotomy-property xy \with {  | inr y<x => ((y :-: x :-: xs', consSorted (byRight y<x) xs-sorted), insertedHere idp idp)  | inl x<=y =>   \let ((result, result-sorted), result-spec) => insert (xs', tail-sorted x xs' xs-sorted) y   \in ((x :-: result, {?}), insertedThere idp result-spec) 

Para un solo fragmento que queda sin prueba, Arend generará el siguiente valor de contexto:

 Expected type: Sorted (x :-: (insert (\this, tail-sorted x \this \this) \this).1.1) Context:  result-sorted : Sorted (insert (\this, tail-sorted \this \this \this) \this).1.1  xs-sorted : Sorted (x :-: xs')  x : O  x<=y : Or (x = y) (O.< xy)  O : Dec  result : List O  y : O  xs' : List O  result-spec : InsertSpec xs' y (insert (xs', tail-sorted \this xs' \this) y).1.1 

Para completar la prueba, tendremos que usar toda la potencia del operador \case : utilizaremos la coincidencia de patrones con 5 variables diferentes, y dado que los tipos de algunas variables pueden depender de los valores de otras variables, utilizaremos la coincidencia de patrones dependientes.

La construcción de dos puntos indica explícitamente cómo el tipo de algunas variables a comparar depende del valor de otras variables (por lo tanto, en el tipo de variables xs-sorted, result-spec y result-sorted en cada \case lugar de xs' y result coincidirá con las muestras correspondientes).

La construcción \return asocia las variables utilizadas para hacer coincidir el patrón con el tipo de resultado esperado. En otras palabras, en el objetivo actual, en cada una de las cláusulas \case , la muestra correspondiente se sustituirá por la variable de result . Sin esta construcción, dicho reemplazo no se llevaría a cabo, y el objetivo de todas las cláusulas \case coincidiría con el objetivo en lugar de la expresión \case misma.

 \func insert {O : LinearOrder.Dec} (xs : SortedList O) (y : O) : \Sigma (ys : SortedList O) (InsertSpec xs.1 y ys.1) \elim xs  | (nil, _) => ((y :-: nil, singletonSorted), insertedHere idp idp)  | (:-: x xs', xs-sorted) => \case set-trichotomy-property xy \with {   | inr y<x => ((y :-: x :-: xs', consSorted (byRight y<x) xs-sorted), insertedHere idp idp)   | inl x<=y =>     \let ((result, result-sorted), result-spec) => insert (xs', tail-sorted x xs' xs-sorted) y     \in ((x :-: result,       \case result \as result, xs' \as xs', xs-sorted : Sorted (x :-: xs'), result-spec : InsertSpec xs' y result, result-sorted : Sorted result       \return Sorted (x :-: result) \with {        | nil, _, _, _, _ => singletonSorted        | :-: r rs, _, _, insertedHere y=r _, result-sorted => consSorted (transport (\lam z => (x = z) || (x < z)) y=r (Or-to-|| x<=y)) result-sorted        | :-: r rs, :-: x' _, consSorted x<=x' _, insertedThere x2=r _, result-sorted => consSorted (transport (\lam z => (x = z) || (x < z)) x2=r x<=x') result-sorted }), insertedThere idp result-spec) 

En el bloque de código anterior, los primeros argumentos complejos del constructor consSorted en los últimos dos párrafos de la comparación de patrones merecen un comentario adicional. Para entender qué significan ambas expresiones, las reemplazamos con la expresión {?} Y le pedimos al temporizador Arend que determine los objetivos en ambas posiciones.

Puede ver que tanto allí como allí el objetivo actual es el tipo (x = r) || O.< xr (x = r) || O.< xr . Además, en el contexto del primer objetivo, hay premisas

 x<=y : Or (x = y) (O.< xy) y=r : y = r 

y en el contexto del segundo - premisas

 x<=x' : (x = x') || O.< xx' x2=r : x' = r. 

Intuitivamente claro: para probar el primer objetivo, es suficiente sustituir la variable r en el enunciado correcto Or (x = y) (O.< xy) , y luego cambiar al tipo truncado proposicionalmente || utilizando la función Or-to-|| definida en la Sección 1.3 . Para probar el segundo objetivo, simplemente sustituya en (x = x') || O.< x x' (x = x') || O.< x x' lugar de la variable x' variable r .

Para formalizar la operación de sustitución de expresión descrita, existe una función de transport especial en la biblioteca estándar de Arend. Considere su firma:

 \func transport {A : \Type} (B : A -> \Type) {aa' : A} (p : a = a') (b : B a) : B a' 

En nuestro caso, el tipo OE debe ser sustituido por la variable A (puede omitirse explícitamente si se especifican los otros argumentos de transport ), y la expresión \lam (z : O) => (x = z) || (x < z) \lam (z : O) => (x = z) || (x < z) .

La implementación del algoritmo de clasificación de inserción junto con la especificación ya no causa dificultades particulares: para ordenar la lista x :-: xs' , primero ordenamos la cola de la lista xs' usando una llamada recursiva a insertSort , y luego insertamos el elemento x dentro de esta lista mientras conservamos el orden cuando Ayuda a acceder a la función de insert ya implementada.

 \func insertSort {O : LinearOrder.Dec} (xs : List O) : \Sigma (result : SortedList O) (Perm xs result.1) \elim xs | nil => ((nil, nilSorted), permTrivial idp) | :-: x xs' => \let | (ys, perm-xs'-ys) => insertSort xs'                      | (zs, zs-spec) => insert ys x                  \in (zs, permInsert perm-xs'-ys (insertedHere idp idp) zs-spec) 

Cumplimos el objetivo inicial e implementamos la clasificación de las listas en Arend. Todo el código Arend que se proporciona en este párrafo se puede descargar en un archivo desde aquí .

Uno podría preguntarse cómo tendría que cambiar la implementación de la función de insert si en lugar de las órdenes estrictas LinearOrder.Dec utilizamos las órdenes no estrictas TotalOrder ? Como recordamos, en la definición de la función de totalidad, el uso de la operación truncada || fue bastante significativo, es decir, esta definición no es equivalente a una definición en la que en lugar de || utilizado por Or .

La respuesta a esta pregunta es la siguiente: todavía es posible construir un análogo de insert para TotalOrder , sin embargo, para esto tendríamos que demostrar que el tipo de la función de insert es una declaración (esto nos permitiría en la definición de insert hacer una comparación con la muestra de acuerdo con la declaración de totality xy ).

En otras palabras, tendríamos que demostrar que solo hay una lista ordenada hasta la igualdad, que es el resultado de insertar el elemento y en la lista ordenada xs . Es fácil ver que este es un hecho cierto, pero su prueba formal ya no es tan trivial. Dejamos la verificación de este hecho como un ejercicio para el lector interesado.

3. Observaciones finales


En esta introducción, nos familiarizamos con las construcciones principales del lenguaje Arend, y también aprendimos cómo usar el mecanismo de clase. Logramos implementar el algoritmo más simple junto con la prueba de su especificación. Por lo tanto, hemos demostrado que Arend es bastante adecuado para resolver problemas "cotidianos", como, por ejemplo, la verificación de programas.

Hemos mencionado lejos de todas las características y características de Arend. Por ejemplo, no dijimos casi nada acerca de los tipos con condiciones que le permiten "pegar" varios constructores de tipos con algunos valores de parámetros especiales para estos constructores. Por ejemplo, se proporciona una implementación del tipo entero en Arend usando tipos con las condiciones siguientes:

 \data Int | pos Nat | neg Nat \with { zero => pos zero } 

Esta definición dice que los enteros consisten en dos copias del tipo de números naturales, en los que se identifican ceros "positivos" y "negativos". Dicha definición es mucho más conveniente que la definición de la biblioteca estándar de Coq, donde la "copia negativa" de los números naturales debe ser "desplazada por uno" para que estas copias no se crucen (es mucho más conveniente cuando la notación neg 1 significa el número -1, no -2) .

No dijimos nada sobre el algoritmo para derivar niveles predicativos y de homotopía en las clases y sus instancias. Tampoco mencionamos el tipo de intervalo I , aunque juega un papel clave en la teoría de los tipos con intervalos, que son la base lógica de Arend. Para comprender lo importante que es este tipo, es suficiente mencionar que en Arend la igualdad de tipos se define a través del concepto de intervalo. , , , (.. ).

: , HoTT JetBrains Research.

Source: https://habr.com/ru/post/470632/


All Articles