什么是依存类型?


82

有人可以向我解释依赖类型吗?我对Haskell,Cayenne,Epigram或其他功能语言的经验很少,因此可以使用的术语越简单,我将越感激!


那么,您对诸如Wikipedia文章的了解到底是什么?
Karl Knechtel '02

124
好吧,本文以lambda多维数据集开头,在我看来,这听起来像某种羊肉。然后继续讨论λΠ2系统,因为我不会说外星人,所以跳过了这一部分。然后,我了解了归纳结构的演算,顺便说一句,它似乎与演算,传热或构造没有任何关系。提供语言比较表后,文章结束,与进入页面相比,我感到更加困惑。
尼克

3
@Nick这是Wikipedia的普遍问题。几年前,我看到了您的评论,此后我一直记得。我现在将其添加为书签。
Daniel H

Answers:


111

考虑一下:在所有不错的编程语言中,您都可以编写函数,例如

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的教程中首先提到。
Narfanar 2015年

3
@Noein,精简类型确实是依赖类型的一种简单形式。
Andreas Rossberg 2015年

21

从属类型可以在编译时消除更大的逻辑错误集。为了说明这一点,请考虑以下有关功能的规范: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显示在值的类型中,proofn.IsEven.type

def f(n: Integer)(implicit proof: n.IsEven.type)
      ^                           ^
      |                           |
    value                       value

我们说类型 n.IsEven.type取决于值, n因此称为依赖类型


5
如何处理随机值?例如,会f(random())导致编译错误吗?
Wong Jia Hau

5
f某个表达式应用于某个表达式将要求编译器(无论有无帮助)都必须使表达式始终为偶数,并且不存在此类证明random()(因为它实际上可能是奇数),因此f(random())将无法编译。
Matthijs

18

如果您碰巧知道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的依存类型语言列表与其定理证明者列表进行比较,您可能会发现可疑的相似性。;-)


4

引用《类型和编程语言》(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 按类型索引的类型族

查看此列表,很显然,我们还没有考虑过一种可能性:按术语索引的类型族。在依赖类型的标题下,这种抽象形式也得到了广泛的研究。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.