Recursion trie上函数的终止检查

Recursion trie上函数的终止检查,recursion,trie,termination,agda,Recursion,Trie,Termination,Agda,我很难说服Agda终止检查下面的函数fmap,以及在Trie结构上递归定义的类似函数。Trie是一个域,其域是一个类型,一个由单元、产品和固定点组成的对象级类型(为了使代码最小化,我省略了副产品)。问题似乎与我在Trie的定义中使用的类型级替换有关。(表达式const(μₜ τ) *τ表示应用替换常数(μₜ τ) 到类型τ) 模块温度,其中 打开导入数据。单位 打开导入类别。函子 开放式导入功能 开放式导入级别 打开导入关系。二进制 --上下文只是一个snoc列表。 数据Cxt{内联/熔丝技巧可

我很难说服Agda终止检查下面的函数
fmap
,以及在
Trie
结构上递归定义的类似函数。
Trie
是一个域,其域是一个
类型
,一个由单元、产品和固定点组成的对象级类型(为了使代码最小化,我省略了副产品)。问题似乎与我在
Trie
的定义中使用的类型级替换有关。(表达式
const(μₜ τ) *τ
表示应用替换
常数(μₜ τ) 
到类型
τ

模块温度,其中
打开导入数据。单位
打开导入类别。函子
开放式导入功能
开放式导入级别
打开导入关系。二进制
--上下文只是一个snoc列表。

数据Cxt{内联/熔丝技巧可以(也许)以令人惊讶的方式应用。此技巧适用于此类问题:

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
此函数在结构上是递归的,但以隐藏的方式。
map
仅将
map trie f
应用于
xs
的元素,因此
map trie
应用于较小的(子-)尝试。但Agda没有查看
map
的定义,以确定它没有做任何奇怪的事情。因此,我们必须应用内联/融合技巧,使其通过终止检查器:

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

您的
fmap
函数共享相同的结构,您映射了某种提升的函数。但是要内联什么呢?如果我们遵循上面的示例,我们应该内联
fmap
本身。这看起来和感觉上都有点奇怪,但实际上,它是有效的:

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

您还可以应用另一种技术:称为大小类型。与其依赖编译器来确定某个内容何时是结构递归的,不如直接指定它。但是,您必须按
Size
类型对数据类型进行索引,因此这种方法相当具有侵入性,无法应用于已经存在的数据类型我认为这是值得一提的

在其最简单的形式中,size type表现为由自然数索引的类型。此索引指定结构大小的上限。您可以将其视为树高度的上限(假定数据类型是某个函子F的F分支树).size版本的
列表
看起来几乎像一个
Vec
,例如:

{-# OPTIONS --sized-types #-}
open import Size
但是大小类型添加了一些使其更易于使用的特性∞
用于不关心大小的情况。
suc
称为
和Agda实现的规则很少,例如
↑ ∞ = ∞

让我们重写
Trie
示例以使用大小类型。我们需要在文件顶部添加一个pragma和一个导入:

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 : ∀ {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)
如果您保持
map trie
功能不变,终止检查器仍会抱怨。这是因为当您不指定任何大小时,Agda将填写无穷大(即不关心值),我们回到了开始

但是,我们可以将
映射trie
标记为保持大小:

因此,如果你给它一个以
i
为界的
Trie
,它也会给你另一个以
i
为界的
Trie
。因此
map Trie
永远不能使
Trie
变大,只能使其变大或变小。这足以让终止检查器找出
map(map Trie f)xs
正常


此技术也可应用于您的
Trie

打开导入大小
更名(↑_ 至^)

data Trie{看起来您的一些unicode正在丢失:(啊,在我安装的Ubuntu上没有;)告诉我哪里有您看不到的字符,我会尝试发布一个更友好的版本。(我想知道是不是像我说的“字母符号”而不是“字体字符”这样的特殊字体字符)。内联技巧实际上是适用的-它只是看起来有点奇怪。可能有更好的方法,但是如果你对这个解决方案满意,我将把它作为一个答案发布。是的,unicode似乎造成了一些麻烦(不适用于我的Agda模式,因为DejaVu Sans+Code2000可以处理几乎任何事情;另一方面,Gist…).Subscript
t
s
没有为我显示(我得到了带有十六进制代码点的框)。看起来stackoverflow的CSS为代码块提供了更多的字体选择。太棒了,非常感谢。我选择了“大小类型”方法,因为内联方法对证明的影响(加上它很可怕:)。如果你将不同大小的值组合在一起,比如说,而不是像
fmap
那样只保留大小,那么大小推理会变得棘手吗?同样好奇的是,“大小类型”是如何例如,这种方法与其他一般策略相比。@Roly:嗯,我不太使用大小类型,所以我不太清楚它们的限制。但我建议您查看Agda存储库中的示例和测试:只需grep for
--size type
。回答得很好。@Roly:我的理解有限,但我的印象是大小类型通常(或总是?)允许阻止“内联/融合”技巧,甚至更多。如果没有大小类型,则在本地检查大小(不考虑调用函数的主体),“内联/融合”解决了这一问题;但是大小类型允许在非本地传播这类信息-它们暴露“大小行为”函数在其接口中的实现,在调用方终止检查期间可见。@Blaisorblade好的,这是一个信息性的解释。谢谢。
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)