通过trie终止检查功能

时间:2014-02-02 15:39:00

标签: recursion trie termination agda

我很难说服Agda终止检查下面的函数fmap,并且在Trie的结构上递归地定义了类似的函数。 Trietrie,其域为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 trick看起来并不特别适用,但也许是这样。或者我应该寻找另一种方法来处理Trie定义中的替换?

1 个答案:

答案 0 :(得分:5)

内联/保险丝技巧可以(可能)以惊人的方式应用。这个技巧适用于这类问题:

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 σ)

您可以应用另一种技术:它称为大小的类型。您可以直接指定它,而不是依赖于编译器来确定somethig何时是结构递归。但是,您必须使用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示例以使用大小的类型。我们需要在文件顶部的一个pragma和一个导入:

{-# 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 σ)

{{1}}