有人可以向我解释依赖类型吗?我对Haskell,Cayenne,Epigram或其他功能语言的经验很少,因此可以使用的术语越简单,我将越感激!
有人可以向我解释依赖类型吗?我对Haskell,Cayenne,Epigram或其他功能语言的经验很少,因此可以使用的术语越简单,我将越感激!
Answers:
考虑一下:在所有不错的编程语言中,您都可以编写函数,例如
def f(arg) = result
在这里,f
取一个值arg
并计算一个值result
。它是从值到值的函数。
现在,一些语言允许您定义多态(也称为通用)值:
def empty<T> = new List<T>()
在这里,empty
采用一个类型T
并计算一个值。它是一个从类型到值的函数。
通常,您还可以具有通用类型定义:
type Matrix<T> = List<List<T>>
该定义采用一个类型,并返回一个类型。可以将其视为功能,从类型到类型。
普通语言所提供的东西太多了。如果一种语言也提供第四种可能性,那就是所谓的依赖类型,即定义从值到类型的函数。或者换句话说,在值上参数化类型定义:
type BoundedInt(n) = {i:Int | i<=n}
一些主流语言具有这种虚假形式,请勿混淆。例如,在C ++中,模板可以将值用作参数,但是在应用时,它们必须是编译时常量。在真正依赖类型的语言中不是这样。例如,我可以这样使用上面的类型:
def min(i : Int, j : Int) : BoundedInt(j) =
if i < j then i else j
在这里,函数的结果类型取决于实际参数值j
,因此取决于术语。
BoundedInt
示例实际上不是精炼类型吗?这是“非常接近”的,但不完全是“依赖类型”的类型,例如,Idris在有关dep.typing的教程中首先提到。
从属类型可以在编译时消除更大的逻辑错误集。为了说明这一点,请考虑以下有关功能的规范:f
函数
f
必须仅接受偶数整数作为输入。
没有依赖类型,您可能会执行以下操作:
def f(n: Integer) := {
if n mod 2 != 0 then
throw RuntimeException
else
// do something with n
}
在这里,编译器无法检测是否n
确实是偶数,也就是说,从编译器的角度来看,以下表达式可以:
f(1) // compiles OK despite being a logic error!
该程序将运行,然后在运行时引发异常,即您的程序有逻辑错误。
现在,从属类型使您更具表达能力,并使您可以编写如下内容:
def f(n: {n: Integer | n mod 2 == 0}) := {
// do something with n
}
这n
是依赖类型的{n: Integer | n mod 2 == 0}
。大声朗读可能会有所帮助
n
是一组整数的成员,这样每个整数都可以被2整除。
在这种情况下,编译器将在编译时检测到您已向其传递了奇数的逻辑错误,f
并首先阻止了程序的执行:
f(1) // compiler error
这是一个使用Scala路径相关类型的说明性示例,说明如何尝试实现f
满足此类要求的功能:
case class Integer(v: Int) {
object IsEven { require(v % 2 == 0) }
object IsOdd { require(v % 2 != 0) }
}
def f(n: Integer)(implicit proof: n.IsEven.type) = {
// do something with n safe in the knowledge it is even
}
val `42` = Integer(42)
implicit val proof42IsEven = `42`.IsEven
val `1` = Integer(1)
implicit val proof1IsOdd = `1`.IsOdd
f(`42`) // OK
f(`1`) // compile-time error
关键是要注意值如何n
显示在值的类型中,proof
即n.IsEven.type
:
def f(n: Integer)(implicit proof: n.IsEven.type)
^ ^
| |
value value
我们说类型 n.IsEven.type
取决于值, n
因此称为依赖类型。
f(random())
导致编译错误吗?
f
某个表达式应用于某个表达式将要求编译器(无论有无帮助)都必须使表达式始终为偶数,并且不存在此类证明random()
(因为它实际上可能是奇数),因此f(random())
将无法编译。
如果您碰巧知道C ++,那么很容易提供一个有启发性的示例:
假设我们有一些容器类型及其两个实例
typedef std::map<int,int> IIMap;
IIMap foo;
IIMap bar;
并考虑以下代码片段(您可以假定foo为非空):
IIMap::iterator i = foo.begin();
bar.erase(i);
这是明显的垃圾(可能破坏了数据结构),但是由于“ iterator into foo”和“ iterator into bar”是同一类型,因此它会进行类型检查, IIMap::iterator
,即使它们在语义上完全不兼容,也。
问题在于,迭代器类型不仅应取决于容器类型,而且实际上应取决于容器对象,即它应该是“非静态成员类型”:
foo.iterator i = foo.begin();
bar.erase(i); // ERROR: bar.iterator argument expected
这样的功能,即表达依赖于术语(foo)的类型(foo.iterator)的能力,正是依赖类型的含义。
之所以很少看到此功能,是因为它打开了一大堆蠕虫:您突然遇到了以下情况:要在编译时检查两种类型是否相同,最终必须证明两个表达式是等效的(在运行时始终会产生相同的值)。结果,如果将Wikipedia的依存类型语言列表与其定理证明者列表进行比较,您可能会发现可疑的相似性。;-)
引用《类型和编程语言》(30.5)一书:
本书的大部分内容都涉及形式化各种抽象机制。在简单类型的lambda演算中,我们对取一个项并抽象出一个子项的操作进行了形式化,生成了一个函数,以后可以通过将其应用于不同的项进行实例化。在System中
F
,我们考虑了获取一个术语并抽象出一个类型,产生可以通过将其应用于各种类型来实例化的术语的操作。在λω
,我们概述了简单类型的lambda演算“一级升级”的机制,采用一个类型并抽象出一个子表达式以获得一个类型运算符,该运算符以后可以通过将其应用于不同的类型进行实例化。考虑所有这些抽象形式的简便方法是根据表达家族,并用其他表达来索引。普通的lambda抽象λx:T1.t2
是按术语[x -> s]t1
索引的术语索引的术语s
。类似地,类型抽象λX::K1.t2
是按类型索引的术语族,而类型运算符是按类型索引的类型族。
λx:T1.t2
术语索引的术语族
λX::K1.t2
按类型索引的术语族
λX::K1.T2
按类型索引的类型族查看此列表,很显然,我们还没有考虑过一种可能性:按术语索引的类型族。在依赖类型的标题下,这种抽象形式也得到了广泛的研究。