归纳类型、余归纳类型与发散类型
(Inductive, Co-inductive, and Recursive Types)
Koka 的 type 关键字用于声明归纳类型(inductive types)——例如 list。为了实现等式推理(equational reasoning)和终止性检查(termination checking),Koka 对普通 type 声明做了严格限制:类型本身不能出现在负位置(negative position)。这一限制保证了归纳类型的良构性,使得编译器可以对操作这类数据的递归函数做终止性分析。
除了 type,Koka 还提供另外两种类型声明:co type(余归纳类型,co-inductive types)和 div type(任意递归类型,arbitrary recursive types)。它们分别放宽了 type 的不同限制,以适应不同的编程需求。
归纳类型(type):有限、正向递归、终止性
普通的 type 声明要求递归是正向的(positive)——即类型自身只能出现在构造子的“输出“位置,而不能出现在“输入“(负)位置。经典的例子是标准库的列表:
pub type list<a>
con Nil
con Cons(head:a, tail : list<a> )
在 Koka 里,
con是用于显式声明“构造子(constructor)“的关键字。PLDB 的 Koka 关键字表明确把con列为语言关键字之一 。现代 Koka 在type定义体内默认把每个顶级声明当作构造子,所以con可以省略。 这里list<a>出现在Cons的tail字段里——这是正位置,合法。这种限制保证了:
- 可终止性:对任何在
list上做结构递归的函数,编译器可以做终止性检查 - 等式推理:归纳类型满足归纳法原理,支持等式推理
以下这种
type声明中bad出现在bad -> int的参数位置——负位置,编辑器诊断会报错、编译会报错
type bad
Bad(f: bad -> int)
Koka 的终止性检查器会分析递归函数:如果每次递归调用都减小了归纳类型参数的大小,则该函数被判定为 total,否则 div。
标准库的 length 实现:
pub fun length(xs)
fun len(ys,acc)
match ys
Cons(_,yy) -> yy.len(acc+1)
Nil -> acc
xs.len(0)
length 在每个递归调用中都处理更短的列表,因此被判定为 total:fun length : forall<a> (xs : list<a>) -> int
余归纳类型(co type):潜在无穷、生产性
有些数据结构本质上是潜在的无穷——例如惰性流(stream)、无限序列、响应式事件流等。这些结构不能用普通的 type 声明,因为 type 要求有限归纳。针对潜在的无穷,Koka 提供 co type 来声明余归纳类型,比如标准库的 stream:
pub co type stream<a>
con Next(head:a, tail: stream<a> )
stream 在数学上就是余归纳类型——它的元素是“通过观察逐步揭示“的,可能无穷(如 repeat(1) 产生的流)。标准库选择 co type 是为了在语义层面精确标记这种“潜在无穷“的性质,语法上不存在“必须用 co type 才能声明“。实际上 type 也可以用于其声明,但是缺少了“潜在无穷“标记,所以在你自定义一个 stream 时,即使去掉co只用type也能通过编译检查。
但实际上是会有产生无穷 stream 的情况的:
// 1. 延迟计算单元:thunk
type thunk<a>
Delay(f: () -> div a) // 未求值
Memo(v: a) // 已求值(用于记忆化,可选)
// 强迫求值
fun force(t: thunk<a>): div a
match t
Delay(f) -> f() // 这里简单地直接调用;生产代码可做 memoization
Memo(v) -> v
// 2. 流类型:tail 是 thunk<stream<a>>
// 用 co type 标记"潜在无穷"的余归纳语义
co type stream<a> //实际上去掉 co 也能通过编译而且不会有任何影响
con SCons(head: a, tail: thunk<stream<a>>)
// 3. 重复单一元素 → 潜在无穷流
// 关键:tail 用 Delay(fn() my-repeat(x)) 包裹,构造时不立即求值
pub fun my-repeat(x: a): <div|_e> stream<a>
SCons(x, Delay(fn() my-repeat(x)))
// 4. 取前 n 个元素,转成 list
pub fun take(s: stream<a>, n: int): <div|_e> list<a>
match n
0 -> Nil
_ -> match s
SCons(h, t) -> Cons(h, take(force(t), n - 1))
// 5. 从初始值迭代:f(x), f(f(x)), f(f(f(x))), ...
pub fun iterate(f: a -> a, x: a): <div|_e> stream<a>
SCons(x, Delay(fn() iterate(f, f(x))))
// 6. main:只操作有限前缀,程序正常终止
pub fun main()
println(take(my-repeat(1), 100)) // 将100改成任何数字n都可以打印出n个1
极性翻转(polarity flipping)
type 和 co type 都要求自身类型不能在负位置
type foo
Foo(f: (foo -> int) -> int)
co type bar
Bar(f: (bar -> int) -> int)
foo、bar位于内层箭头的左侧(负位),而该箭头整体又位于外层箭头的左侧(负位) 负 × 负 = 正 检查通过。
发散类型(div type):放开、放弃自动终止性
如果需要任意的递归类型——包括类型出现在负位置(如函数参数位置),Koka 提供 div type:
div type foo
Foo(recur: foo -> int)
div type 允许类型以任意方式递归,包括出现在负位置。这使得可以利用 Koka 允许的命名函数 + 递归数据类型,构造出了一个自引用的不动点,表达能力等效于 Y 组合子:
div type foo
Foo(recur: foo -> int)
fun upsilon(x: foo): int
match x
Foo(recur) -> recur(x)
fun do_upsilon(): int
upsilon(Foo(upsilon))
这段代码执行会导致无限递归,导致栈溢出, exit code -11 即 SIGSEGV(段错误)。 任何对
div type做模式匹配的函数,都被假定为可能发散(potentially divergent),因此自动带上div效应。这是一个保守但安全的设计——因为div type赋予了编码 Y 组合子的能力,可以写出非语法递归的不终止函数,所以 Koka 必须假定这类函数可能发散。