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