Завершение проверки функции по дереву

Мне трудно убедить Agda выполнить проверку завершения функции fmap ниже и аналогичных функций, определенных рекурсивно в структуре Trie. Trie - это дерево, домен которого является Type , тип уровня объекта, состоящий из единицы, продуктов и фиксированных точек (я пропустил сопутствующие продукты, чтобы код был минимальным). Проблема, похоже, связана с заменой на уровне типа, которую я использую в определении Trie. (Выражение const (μₜ τ) * τ означает применение подстановки const (μₜ τ) к типу τ.)

module Temp where

   open import Data.Unit
   open import Category.Functor
   open import Function
   open import Level
   open import Relation.Binary

   -- A context is just a snoc-list.
   data Cxt {????} (A : Set ????) : Set ???? where
      ε : Cxt A
      _∷ᵣ_ : Cxt A → A → Cxt A

   -- Context membership.
   data _∈_ {????} {A : Set ????} (a : A) : Cxt A → Set ???? where
      here : ∀ {Δ} → a ∈ Δ ∷ᵣ a
      there : ∀ {Δ a′} → a ∈ Δ → a ∈ Δ ∷ᵣ a′
   infix 3 _∈_

   -- Well-formed types, using de Bruijn indices.
   data _⊦ (Δ : Cxt ⊤) : Set where
      nat : Δ ⊦
      ???? : Δ ⊦
      var : _ ∈ Δ → Δ ⊦
      _+_ _⨰_ : Δ ⊦ → Δ ⊦ → Δ ⊦
      μ : Δ ∷ᵣ _ ⊦ → Δ ⊦
   infix 3 _⊦

   -- A closed type.
   Type : Set
   Type = ε ⊦

   -- Type-level substitutions and renamings.
   Sub Ren : Rel (Cxt ⊤) zero
   Sub Δ Δ′ = _ ∈ Δ → Δ′ ⊦
   Ren Δ Δ′ = ∀ {x} → x ∈ Δ → x ∈ Δ′

   -- Renaming extension.
   extendᵣ : ∀ {Δ Δ′} → Ren Δ Δ′ → Ren (Δ ∷ᵣ _) (Δ′ ∷ᵣ _)
   extendᵣ ρ here = here
   extendᵣ ρ (there x) = there (ρ x)

   -- Lift a type renaming to a type.
   _*ᵣ_ : ∀ {Δ Δ′} → Ren Δ Δ′ → Δ ⊦ → Δ′ ⊦
   _ *ᵣ nat = nat
   _ *ᵣ ???? = ????
   ρ *ᵣ (var x) = var (ρ x)
   ρ *ᵣ (τ₁ + τ₂) = (ρ *ᵣ τ₁) + (ρ *ᵣ τ₂)
   ρ *ᵣ (τ₁ ⨰ τ₂) = (ρ *ᵣ τ₁) ⨰ (ρ *ᵣ τ₂)
   ρ *ᵣ (μ τ) = μ (extendᵣ ρ *ᵣ τ)

   -- Substitution extension.
   extend : ∀ {Δ Δ′} → Sub Δ Δ′ → Sub (Δ ∷ᵣ _) (Δ′ ∷ᵣ _)
   extend θ here = var here
   extend θ (there x) = there *ᵣ (θ x)

   -- Lift a type substitution to a type.
   _*_ : ∀ {Δ Δ′} → Sub Δ Δ′ → Δ ⊦ → Δ′ ⊦
   θ * nat = nat
   θ * ???? = ????
   θ * var x = θ x
   θ * (τ₁ + τ₂) = (θ * τ₁) + (θ * τ₂)
   θ * (τ₁ ⨰ τ₂) = (θ * τ₁) ⨰ (θ * τ₂)
   θ * μ τ = μ (extend θ * τ)

   data Trie {????} (A : Set ????) : Type → Set ???? where
      〈〉 : A → ???? ▷ A
      〔_,_〕 : ∀ {τ₁ τ₂} → τ₁ ▷ A → τ₂ ▷ A → τ₁ + τ₂ ▷ A
      ↑_ : ∀ {τ₁ τ₂} → τ₁ ▷ τ₂ ▷ A → τ₁ ⨰ τ₂ ▷ A
      roll : ∀ {τ} → (const (μ τ) * τ) ▷ A → μ τ ▷ A

   infixr 5 Trie
   syntax Trie A τ = τ ▷ A

   {-# NO_TERMINATION_CHECK #-}
   fmap : ∀ {a} {A B : Set a} {τ} → (A → B) → τ ▷ A → τ ▷ B
   fmap f (〈〉 x)    = 〈〉 (f x)
   fmap f 〔 σ₁ , σ₂ 〕 = 〔 fmap f σ₁ , fmap f σ₂ 〕
   fmap f (↑ σ)    = ↑ (fmap (fmap f) σ)
   fmap f (roll σ) = roll (fmap f σ)

Казалось бы, fmap рекурсивно превращается в аргумент строго меньшего размера в каждом случае; конечно, случай продукта в порядке, если я удалю рекурсивные типы. С другой стороны, определение нормально обрабатывает рекурсивные типы, если я удаляю продукты.

Какой самый простой способ действовать здесь? Уловка inline / fuse не выглядит особенно применимой, но, возможно, так и есть. Или мне следует искать другой способ справиться с заменой в определении Trie?


person Roly    schedule 02.02.2014    source источник
comment
Похоже, кое-что из вашего юникода теряется :(   -  person copumpkin    schedule 02.02.2014
comment
Ах. Не на моей установке Ubuntu;) Скажите, где есть символы, которых вы не видите, и я постараюсь опубликовать более дружелюбную версию. (Интересно, это специальные символы шрифта, такие как ?????)   -  person Roly    schedule 02.02.2014
comment
Я имел в виду буквенные символы, а не символы шрифта.   -  person Roly    schedule 02.02.2014
comment
Встроенный трюк действительно применим - он просто выглядит немного странно. Возможно, есть способ получше, но если вам нравится это решение, я отправлю его в качестве ответа. И да, юникод, похоже, вызывает некоторые проблемы (не для моего Agda-режима, поскольку DejaVu Sans + Code2000 обрабатывает практически все; Gist с другой стороны ...). gist.github.com/vituscze/8773710   -  person Vitus    schedule 02.02.2014
comment
Большое спасибо. Сейчас я проверяю, могу ли я обобщить ваше решение для моего существующего кода. Кстати, что не отображается должным образом? (К сожалению, Gist мне тоже нравится.)   -  person Roly    schedule 03.02.2014
comment
Нижний индекс t и s не отображаются для меня (я получаю поля с шестнадцатеричным кодом). Похоже, что в CSS stackoverflow больше вариантов шрифтов для блоков кода.   -  person Vitus    schedule 03.02.2014
comment
Хорошо спасибо. Я в значительной степени отказался от этих индексов (они мне не очень нравились), так что это дает мне вескую причину отказаться от одного или двух оставшихся. У меня возникла проблема с вашим решением, когда я попытался снова добавить свой базовый тип Nat - причина в том, что дерево от Nat до A реализовано как FiniteMap от Nat до A, а экземпляр функтора для trie делегирует экземпляр функтора для FiniteMap. Аарх. Может, мне и это нужно частично встроить ... Я еще поэкспериментирую.   -  person Roly    schedule 03.02.2014
comment
Хорошо, мне удалось заставить его работать, добавив дополнительный шаг. Это выглядит немного неприятно (модульность несколько вылетает из окна), но это работает. Предлагаю добавить свой код в качестве ответа; когда я приму это, я, вероятно, проясню, каково было полное решение.   -  person Roly    schedule 03.02.2014
comment
Кстати выложил новую версию вопроса, без проблемных индексов. (Сообщите мне, есть ли где-нибудь еще шестиугольники.)   -  person Roly    schedule 03.02.2014
comment
Что ж, на Гисте больше нет шестигранников, так что это хорошо. Я бы сказал, что инлайнинг-трансформация кажется неприятной, несмотря ни на что. Он также может работать с размерными типами, но это довольно мешает определениям данных. Я посмотрю, сработает ли это, и если это так, я тоже включу это в ответ.   -  person Vitus    schedule 03.02.2014
comment
Спасибо. Да, я только что сделал это для другой функции, и это действительно меня не устраивает. Я также задавался вопросом, может ли помочь использование сред типов вместо подстановок, поскольку оно заменит использование рекурсивной функции (в определении Trie) индексом, отслеживающим среду. К сожалению, я не совсем понимаю, как работает средство проверки завершения, поэтому я не знаю, поможет ли это на самом деле, но я, вероятно, попробую.   -  person Roly    schedule 03.02.2014


Ответы (1)


Уловку inline / fuse можно применить (возможно) неожиданным образом. Этот трюк подходит для задач такого рода:

data Trie (A : Set) : Set where
  nil  :                     Trie A
  node : A → List (Trie A) → Trie A

map-trie : {A B : Set} → (A → B) → Trie A → Trie B
map-trie f nil = nil
map-trie f (node x xs) = node (f x) (map (map-trie f) xs)

Эта функция является структурно рекурсивной, но скрытым образом. map просто применяет map-trie f к элементам xs, поэтому map-trie применяется к меньшим (под-) попыткам. Но Agda не просматривает определение map, чтобы убедиться, что он не делает ничего необычного. Поэтому мы должны применить трюк с встроенным / предохранителем, чтобы пройти проверку завершения:

map-trie : {A B : Set} → (A → B) → Trie A → Trie B
map-trie         f nil = nil
map-trie {A} {B} f (node x xs) = node (f x) (map′ xs)
  where
  map′ : List (Trie A) → List (Trie B)
  map′ [] = []
  map′ (x ∷ xs) = map-trie f x ∷ map′ xs

Ваша fmap функция имеет ту же структуру, вы сопоставляете какую-то приподнятую функцию. Но что встроить? Если мы последуем примеру выше, мы должны встроить сам fmap. Это выглядит и кажется немного странным, но действительно работает:

fmap fmap′ : ∀ {a} {A B : Set a} {τ} → (A → B) → τ ▷ A → τ ▷ B

fmap  f (〈〉 x) = 〈〉 (f x)
fmap  f 〔 σ₁ , σ₂ 〕 = 〔 fmap f σ₁ , fmap f σ₂ 〕
fmap  f (↑ σ) = ↑ (fmap (fmap′ f) σ)
fmap  f (roll σ) = roll (fmap f σ)

fmap′ f (〈〉 x) = 〈〉 (f x)
fmap′ f 〔 σ₁ , σ₂ 〕 = 〔 fmap′ f σ₁ , fmap′ f σ₂ 〕
fmap′ f (↑ σ) = ↑ (fmap′ (fmap f) σ)
fmap′ f (roll σ) = roll (fmap′ f σ)

Вы можете применить еще один прием: он называется типами размера. Вместо того, чтобы полагаться на компилятор, чтобы выяснить, когда что-то является или не является структурно рекурсивным, вы вместо этого указываете это напрямую. Однако вы должны индексировать свои типы данных по типу Size, поэтому этот подход довольно навязчив и не может быть применен к уже существующим типам, но я думаю, что о нем стоит упомянуть.

В простейшей форме размерный тип ведет себя как тип, индексированный натуральным числом. Этот индекс определяет верхнюю границу структурных размеров. Вы можете думать об этом как о верхней границе высоты дерева (учитывая, что тип данных является F-ветвлением для некоторого функтора F). Увеличенная версия List выглядит почти как Vec, например:

data SizedList (A : Set) : ℕ → Set where
  []  : ∀ {n} → SizedList A n
  _∷_ : ∀ {n} → A → SizedList A n → SizedList A (suc n)

Но типоразмеры добавляют несколько функций, которые упрощают их использование. У вас есть константа на тот случай, если вам не важен размер. suc называется , и Agda реализует несколько правил, например ↑ ∞ = ∞.

Давайте перепишем пример Trie, чтобы использовать размерные типы. Нам понадобится прагма вверху файла и один импорт:

{-# OPTIONS --sized-types #-}
open import Size

А вот и измененный тип данных:

data Trie (A : Set) : {i : Size} → Set where
  nil  : ∀ {i}                         → Trie A {↑ i}
  node : ∀ {i} → A → List (Trie A {i}) → Trie A {↑ i}

Если вы оставите функцию map-trie как есть, средство проверки завершения все равно будет жаловаться. Это потому, что, когда вы не укажете какой-либо размер, Agda заполнит бесконечность (то есть значение безразличия), и мы вернемся к началу.

Однако мы можем отметить map-trie как сохраняющий размер:

map-trie : ∀ {i A B} → (A → B) → Trie A {i} → Trie B {i}
map-trie f nil         = nil
map-trie f (node x xs) = node (f x) (map (map-trie f) xs)

Итак, если вы дадите ему Trie, ограниченный i, он также даст вам другой Trie, ограниченный i. Таким образом, map-trie никогда не сможет сделать Trie больше, только одинаково большим или меньшим. Этого достаточно для проверки завершения, чтобы выяснить, что map (map-trie f) xs в порядке.


Этот метод также можно применить к вашему Trie:

open import Size
  renaming (↑_ to ^_)

data Trie {????} (A : Set ????) : {i : Size} → Type → Set ???? where
  〈〉    : ∀ {i} → A →
    Trie A {^ i} ????
  〔_,_〕 : ∀ {i τ₁ τ₂} → Trie A {i} τ₁ → Trie A {i} τ₂ →
    Trie A {^ i} (τ₁ + τ₂)
  ↑_    : ∀ {i τ₁ τ₂} → Trie (Trie A {i} τ₂) {i} τ₁ →
    Trie A {^ i} (τ₁ ⨰ τ₂)
  roll  : ∀ {i τ} → Trie A {i} (const (μ τ) * τ) →
    Trie A {^ i} (μ τ)

infixr 5 Trie
syntax Trie A τ = τ ▷ A

fmap : ∀ {i ????} {A B : Set ????} {τ} → (A → B) → Trie A {i} τ → Trie B {i} τ
fmap f (〈〉 x) = 〈〉 (f x)
fmap f 〔 σ₁ , σ₂ 〕 = 〔 fmap f σ₁ , fmap f σ₂ 〕
fmap f (↑ σ) = ↑ fmap (fmap f) σ
fmap f (roll σ) = roll (fmap f σ)
person Vitus    schedule 02.02.2014
comment
Потрясающе, очень ценю это. Я выбрал подход размерных типов из-за влияния на доказательства подхода встраивания (плюс это ужасно :). Может ли рассуждение о размере усложниться, если вы, скажем, комбинируете значения разных размеров, а не просто сохраняете размер, как в случае с fmap? - person Roly; 03.02.2014
comment
Также любопытно, как подход размерных типов сравнивается с другими общими стратегиями, например Бове и Венанцио. - person Roly; 03.02.2014
comment
@Roly: Ну, я не часто использую размерные типы, поэтому я не очень хорошо знаю их пределы. Но я предлагаю вам ознакомиться с примерами и тестами в репозитории Agda: code.haskell.org/Agda/examples и code.haskell.org/Agda/test Просто введите --sized-types с помощью grep. - person Vitus; 03.02.2014
comment
Я сделаю это. Спасибо. - person Roly; 03.02.2014
comment
@Vitus всегда впечатлял глубиной ваших ответов. У тебя есть блог или что-то в этом роде? :) - person copumpkin; 10.02.2014
comment
@copumpkin: Всегда приятно слышать, спасибо! :) И нет, у меня нет блога. - person Vitus; 10.02.2014
comment
Отличный ответ. @Roly: мое понимание ограничено, но у меня сложилось впечатление, что размерные типы обычно (или всегда?) Позволяют предотвратить трюк со встроенным / предохранителем и, возможно, больше. Без размерных типов размеры проверяются локально (без учета тел вызываемых функций), и inline / fuse работает вокруг этого; но типы размера позволяют распространять такую ​​информацию нелокально - они раскрывают поведение размера реализации функции в ее интерфейсе, где это видно во время проверки завершения вызывающих. - person Blaisorblade; 09.06.2016
comment
@Blaisorblade Хорошо, это информативное объяснение. Спасибо. - person Roly; 10.06.2016