¿Cómo se puede construir una tabla de memorización coinductiva para funciones recursivas sobre árboles binarios?

8

La biblioteca StreamMemo para Coq ilustra cómo memorizar una función f : nat -> Asobre los números naturales. En particular cuando f (S n) = g (f n), imemo_makecomparte el cálculo de llamadas recursivas.

Supongamos que, en lugar de números naturales, queremos memorizar funciones recursivas sobre árboles binarios:

Inductive binTree : Set := | Leaf : binTree | Branch : binTree -> binTree -> binTree.

Supongamos que tenemos una función f : binTree -> Aque es estructuralmente recursiva, lo que significa que hay una función g : A -> A -> Atal que f (Branch x y) = g (f x) (f y). ¿Cómo construimos una tabla de notas similar para fCoq de modo que se compartan los cálculos recursivos?

En Haskell, no es demasiado difícil construir una tabla de notas (ver MemoTrie por ejemplo) y atar el nudo. Claramente, tales tablas de notas son productivas. ¿Cómo podemos organizar las cosas para convencer a un lenguaje mecanografiado de manera dependiente de que acepte que este nudo sea productivo?

Aunque especifiqué el problema en Coq, me encantaría recibir una respuesta en Agda o en cualquier otro lenguaje de tipo dependiente.

Russell O'Connor
fuente

Respuestas:

4

Es bastante fácil hacer que el patrón de recursión funcione con tipos de tamaño. ¡Esperemos que el intercambio se conserve a través de la compilación! [1]

module _ where

open import Size
open import Data.Nat

data BT (i : Size) : Set where
  Leaf : BT i
  Branch : ∀ {j : Size< i} → BT j → BT j → BT i

record Memo (A : Set) (i : Size) : Set where
  coinductive
  field
    leaf : A
    branch : ∀ {j : Size< i} → Memo (Memo A j) j

open Memo

trie : ∀ {i} {A} → (BT i → A) → Memo A i
trie f .leaf = f Leaf
trie f .branch = trie (\ l → trie \ r → f (Branch l r))

untrie : ∀ {i A} → Memo A i → BT i → A
untrie m Leaf         = m .leaf
untrie m (Branch l r) = untrie (untrie (m .branch) l) r

memo : ∀ {i A} → (BT i → A) → BT i → A
memo f = untrie (trie f)

memoFix : ∀ {A : Set} → A → (A → A → A) → ∀ {i} → BT i → A
memoFix {A} lf br = go
 where
  go h : ∀ {i} → BT i → A
  go = memo h
  h Leaf = lf
  h (Branch l r) = br (go l) (go r)

[1] https://github.com/agda/agda/issues/2918

Saizan
fuente
Gracias por esto. Tengo dos preocupaciones sobre este código. En primer lugar, el govalor es una función de un parámetro Tamaño. En general, no se comparte entre llamadas a funciones independientes con el mismo valor. Esto probablemente se puede solucionar agregando una declaración let en la definición de h (Branch l r). En segundo lugar, la definición estratificada de BTsignifica que dos, de otro modo árboles idénticos, tendrán valores diferentes cuando ocurran en niveles diferentes. Estos valores distintos no se compartirán en el MemoTrie.
Russell O'Connor el
Estoy impresionado de que Agda acepte la definición anidada de Memoin branch. El verificador de positividad de Coq parece rechazar esto, haciendo las cosas más complicadas en Coq.
Russell O'Connor el
El problema al que me vinculé parece concluir que los tamaños no son un problema en tiempo de ejecución cuando se compilan con el backend de GHC, aunque no lo he verificado yo mismo.
Saizan
Veo. Estoy buscando una solución de memorización que pueda usarse dentro del asistente de pruebas para que pueda usarse como parte de una prueba por reflexión. Su solución probablemente sea adecuada para la compilación, suponiendo que los Sizetipos terminen borrados.
Russell O'Connor
0

He creado una "solución" que memoriza recursivamente funciones estructuralmente recursivas de árboles binarios en Coq. Mi esencia está en https://gist.github.com/roconnor/286d0f21af36c2e97e74338f10a4315b .

Funciona de manera similar a la solución de Saizan , estratificando árboles binarios basados ​​en una métrica de tamaño, en mi caso contando el número de nodos internos de árboles binarios, y produciendo un flujo lento de contenedores de todas las soluciones para un tamaño particular. El intercambio se produce debido a una declaración let en el generador de flujo que contiene la parte inicial del flujo que se utilizará en partes posteriores del flujo.

Los ejemplos muestran que vm_compute, para evaluar un árbol binario perfecto con 8 niveles después de haber evaluado un árbol binario perfecto con 9 niveles es mucho más rápido que solo evaluar un árbol binario perfecto con 8 niveles.

Sin embargo, dudo en aceptar esta respuesta porque la sobrecarga de esta solución en particular es mala, ya que funciona mucho peor que mi memorización sin recurrencia estructural para mis ejemplos de aportes prácticos. Naturalmente, quiero una solución que funcione mejor con entradas razonables.

Tengo algunos comentarios adicionales en " [Coq-Club] ¿Cómo puedes construir una tabla de memorización coinductiva para funciones recursivas sobre árboles binarios? ".

Russell O'Connor
fuente