对纯/依赖类型系统的简要但完整的解释是什么?


32

如果简单,那么只需几句话就可以完全解释。可以对λ微积分进行此操作:

λ演算是具有归约规则的语法(基本上是结构)(这意味着对特定模式的每次出现都会重复应用搜索/替换过程,直到不存在这种模式为止)。

语法:

Term = (Term Term) | (λ Var . Term) | Var

归约规则:

((λ var body) term) -> SUBS(body,var,term)
    where `SUBS` replaces all occurrences of `var`
    by `term` in `body`, avoiding name capture.

例子:

(λ a . a)                             -> (λ a a)
((λ a . (λ b . (b a))) (λ x . x))     -> (λ b . (b (λ x x)))
((λ a . (a a)) (λ x . x))             -> (λ x . x)
((λ a . (λ b . ((b a) a))) (λ x . x)) -> (λ b . ((b (λ x . x)) (λ x . x)))
((λ x . (x x)) (λ x . (x x)))         -> never halts

尽管有些非正式,但有人可能认为这对于普通人来说足以理解整个λ演算- 涉及22行减价。我试图理解Idris / Agda和类似项目使用的纯/依赖类型系统,但是我能找到的更简单的解释是Simply Easy-一篇很棒的论文,但这似乎假设了很多以前的知识(Haskell,归纳法定义),我没有。我认为,简短一些,少一些钱可以消除一些障碍。从而,

是否有可能以与我上面介绍的λ微积分相同的格式对纯/依赖类型系统进行简短,完整的解释?


4
纯类型系统的规则非常简短。Simply Easy与实现依赖类型有关。

2
因此,从进攻的意义上说,这不是“敌对的”,但从某种意义上说,您认为我要求没有为自己找到答案而付出足够的努力吗?如果真是这样,我同意这个问题可能会提出很多要求,所以也许是不好的。但是背后还有很多工作,您认为我应该尝试吗?
MaiaVictor

3
我也代表写过“依赖类型的Lambda演算的教程实现”的文本的共同作者而感到冒犯,该文本取代了“ Simply Easy”作为工作标题。我编写了代码的内核,这是Haskell中不到100行的类型检查器。

2
然后我当然表现得很差。我喜欢“ Simply Easy”论文,并且从几天前开始就不停地阅读它-这是世界上唯一让我有些不安的东西,我开始理解这个主题(并相信我已经尝试过) 。但是我确实认为它面向的是比我拥有更多知识的公众,这可能就是为什么我仍然难以参与其中。与纸张质量无关,但有我自己的限制。
MaiaVictor

1
@pigworker和代码是我最喜欢的部分,正是因为它(相对于英语解释)是对整个过程的简短而完整的解释,正如我在这里所问的那样。您碰巧有我可以下载的代码副本吗?
MaiaVictor

Answers:


7

免责声明

根据您的要求,这是非常非正式的。

语法

在依赖类型的语言中,我们在类型级别和值级别都有一个绑定器:

Term = * | (∀ (Var : Term). Term) | (Term Term) | (λ Var. Term) | Var

类型良好的术语是带有附加类型的术语,我们将编写t ∈ σ

σ
t

表示术语t具有类型σ

打字规则

为了简单起见,我们需要在λ v. t ∈ ∀ (v : σ). τ这两个λ绑定相同的变量(v在这种情况下)。

规则:

t ∈ σ is well-formed if σ ∈ * and t is in normal form (0)

*            ∈ *                                                 (1)
∀ (v : σ). τ ∈ *             -: σ ∈ *, τ ∈ *                     (2)
λ v. t       ∈ ∀ (v : σ). τ  -: t ∈ τ                            (3)
f x          ∈ SUBS(τ, v, x) -: f ∈ ∀ (v : σ). τ, x ∈ σ          (4)
v            ∈ σ             -: v was introduced by ∀ (v : σ). τ (5)

因此,*是“所有类型的类型”(1),从类型(2)形成类型,lambda抽象具有pi类型(3),如果v由引入∀ (v : σ). τ,则v具有类型σ(5)。

“正常形式”表示我们使用归约规则执行尽可能多的归约:

“减”法则

(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ) ~> SUBS(b, v, t) ∈ SUBS(τ, v, t)
    where `SUBS` replaces all occurrences of `v`
    by `t` in `τ` and `b`, avoiding name capture.

或在二维语法中

σ
t

表示t ∈ σ

(∀ (v : σ). τ) σ    SUBS(τ, v, t)
                 ~>
(λ  v     . b) t    SUBS(b, v, t)

只有当术语与关联的forall量词中的变量具有相同类型时,才可以对该术语应用lambda抽象。然后,我们以与之前纯lambda演算相同的方式减少了lambda抽象和forall量词。减去值级别部分后,我们得到(4)键入规则。

一个例子

这是函数应用程序运算符:

∀ (A : *) (B : A -> *) (f : ∀ (y : A). B y) (x : A). B x
λ  A       B            f                    x     . f x

(如果未提及,我们缩写∀ (x : σ). τ为)σ -> ττx

f返回B y任何提供y的type A。我们应用fx,这是正确的类型A,并替代yx.,从而f x ∈ SUBS(B y, y, x)〜> f x ∈ B x

现在让我们缩写函数应用运算符as app并将其应用于自身:

∀ (A : *) (B : A -> *). ?
λ  A       B          . app ? ? (app A B)

我放置?我们需要提供的术语。首先,我们明确引入并实例化AB

∀ (f : ∀ (y : A). B y) (x : A). B x
app A B

现在我们需要统一我们拥有的

∀ (f : ∀ (y : A). B y) (x : A). B x

这与

(∀ (y : A). B y) -> ∀ (x : A). B x

什么app ? ?接收

∀ (x : A'). B' x

这导致

A' ~ ∀ (y : A). B y
B' ~ λ _. ∀ (x : A). B x -- B' ignores its argument

(另请参见什么是谓语?

我们的表达(经过重命名)变为

∀ (A : *) (B : A -> *). ?
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

因为对于任何ABf(其中f ∈ ∀ (y : A). B y

∀ (y : A). B y
app A B f

我们可以实例化AB获取(对于任何f具有适当类型的对象)

∀ (y : ∀ (x : A). B x). ∀ (x : A). B x
app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) f

并且类型签名等效于(∀ (x : A). B x) -> ∀ (x : A). B x

整个表达式是

∀ (A : *) (B : A -> *). (∀ (x : A). B x) -> ∀ (x : A). B x
λ  A       B          . app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B)

∀ (A : *) (B : A -> *) (f : ∀ (x : A). B x) (x : A). B x
λ  A       B            f                    x     .
    app (∀ (x : A). B x) (λ _. ∀ (x : A). B x) (app A B) f x

毕竟,在价值水平上的减少会产生相同的app回报。

因此,虽然只需要在纯演算几步就获得app来自app app中,类型化的设置(特别为依赖类型),我们还需要关心的统一,事情变得更加复杂,甚至一些inconsitent方便(* ∈ *)。

类型检查

  • 如果t是,*则按t ∈ *(1)
  • 如果t∀ (x : σ) τσ ∈? *τ ∈? *(见有关注意事项∈?如下图),然后t ∈ *按(2)
  • 如果tf xf ∈ ∀ (v : σ) τ对于στx ∈? σt ∈ SUBS(τ, v, x)乘以(4)
  • 如果t是一个变量vv被引入通过∀ (v : σ). τ然后t ∈ σ通过(5)

这些都是推理规则,但是我们对lambda不能做相同的事情(对于依赖类型,类型推理是不确定的)。因此,对于lambda,我们检查(t ∈? σ)而不是推断:

  • 如果tλ v. b并对照∀ (v : σ) τb ∈? τt ∈ ∀ (v : σ) τ
  • 如果t还有其他问题并进行检查,σ则推断出t使用上述函数的类型,并检查是否为σ

类型的相等性检查要求它们采用正常形式,因此要确定是否t具有类型,σ我们首先要检查σ具有type *。如果是这样,则σ可以归一化(模吉拉德悖论),并且可以归一化(因此σ由(0)形成良好的格式)。SUBS还规范化表达式以保留(0)。

这称为双向类型检查。有了它,我们不需要为每个lambda注释一个类型:如果已知f x类型f为,则x对照参数freceive 的类型进行检查,而不是对其进行推断和比较是否相等(这同样效率较低)。但是,如果f是lambda,则确实需要显式类型注释(语法中省略注释,并且在任何地方都可以添加Ann Term Term或添加λ' (σ : Term) (v : Var)到构造函数中)。

另外,看看“ 更简单,更简单”!博客文章。


1
其次是“更简单,更容易”。

关于forall的第一个减少规则看起来很奇怪。与lambda不同,foralls不应以正确的方式应用(对吗?)。

@chi,我不明白你在说什么。也许我的说法是不好的:归约规则显示(λ v. b ∈ ∀ (v : σ). τ) (t ∈ σ)〜> SUBS(b, v, t) ∈ SUBS(τ, v, t)
user3237465 2015年

1
我发现该符号具有误导性。似乎您有两个规则:一个用于废话(∀ (v : σ). τ) t ~> ...,另一个用于有意义(λ v. b) t ~> ...。我将删除第一个,并在下面将其变成注释。

1
规则(1)包含其结论作为前提。只有在系统正常运行后,才能将系统的简单性与双向版本进行比较。您可能会说您将所有内容都标准化了,但是您的规则却没有。

24

我们去吧 我不会理会吉拉德的悖论,因为它分散了中心思想。我将需要介绍一些有关判断和推导等的表达方式。

语法

术语:: =(Elim)| * | (Var:Term)→条款| λVar↦Term

以林::: Term:Term | Var | 期限

语法有两种相互定义的形式,即“术语”,它们是事物的一般概念(类型是事物,值是事物),包括*(类型的类型),从属函数类型和lambda抽象,但也嵌入了“消除”(即“用法”而不是“构造”),它们是嵌套的应用程序,其中最终位于函数位置的事物是变量或带有其类型注释的术语。

减少规则

(λy↦t:(x:S)→T)s↝t [s:S / y]:T [s:S / x]

(t:T)↝t

替换操作t [e / x]将变量x的每次出现都替换为消除符e,从而避免了名称捕获。要形成可以减少的应用程序,必须在lambda 的类型上加上注释以消除。类型注释为lambda抽象提供了一种“反应性”,从而允许应用程序继续进行。一旦我们到达不再发生应用程序的地步,并且将活动t:T嵌入到术语语法中,就可以删除类型注释。

让我们通过结构闭包来扩展↝减少关系:这些规则适用于术语和消除中的任何地方,您可以找到与左侧匹配的东西。为re的自反传递(0步或更多步)闭包写↝*。从这个意义上讲,最终的还原系统是融合的:

如果s * p和s * q,则存在一些r,使得p ** r和q ** r。

语境

上下文:: = | 上下文,变量:术语

上下文是为变量分配类型的序列,在右边增长,我们认为这是“本地”端,它告诉我们最近绑定的变量。上下文的重要属性是始终可以选择上下文中尚未使用的变量。我们认为上下文中的变量归因类型是不同的。

判决书

判断:: =上下文⊢术语有术语| 上下文⊢Elim是术语

那是判断的语法,但是如何阅读它们呢?首先,⊢是传统的“旋转门”符号,将假设与结论分开:您可以将其非正式地理解为“说”。

G⊢T有t

表示给定上下文G,类型T接受项t;

G⊢e是S

表示给定上下文G,消除e给定类型S.

判断具有有趣的结构:零或多个输入,一个或多个主题,零或多个输出

INPUTS                   SUBJECT        OUTPUTS
Context |- Term   has    Term
Context |-               Elim      is   Term

也就是说,我们必须提前提出术语的类型并检查它们,但是我们综合了消除的类型。

打字规则

我以模糊的Prolog风格呈现这些内容,写成J-:P1; ...; 如果前提P1至Pn也成立,则表示判断J成立的Pn。一个前提将是另一个判断或关于减少的主张。

条款

G⊢T有t-:T↝R; G⊢R有t

G⊢*有*

G⊢*有(x:S)→T-:G⊢*有S; G,z:S!-*具有T [z / x]

G⊢(x:S)→T有λy↦t-:G,z:S⊢T [z / x]有t [z / y]

G⊢T具有(e)-:G⊢e是T

淘汰赛

G⊢e是R-:G is e是S; ↝

G,x:S,G'x是S

G⊢fs是T [s:S / x]-:G⊢f是(x:S)→T; G⊢S有s

就是这样!

只有两个规则不是语法定向的:说“可以先减少类型再使用它来检查术语”的规则,以及说“可以在消除后合成类型后再减少类型”的规则。一种可行的策略是减少类型,直到您公开了最顶层的构造函数。

该系统不是严格归一化的(因为吉拉德悖论是自参照的骗子式悖论),但是可以通过将*拆分为“宇宙层次”来进行强烈归一化,其中任何涉及低层次类型的值具有较高级别的类型,从而防止自我引用。

但是,从这个意义上说,该系统确实具有类型保留的属性。

如果G⊢T有t,G↝* D和T↝R和t↝r,则D⊢R有r。

如果G⊢e是S且G↝* D和e↝f,则存在R使得S↝* R和D⊢f是R。

上下文可以通过允许它们包含的术语来计算。也就是说,如果一个判断现在有效,那么您可以随意计算其输入和其主题,然后可以以某种方式计算其输出以确保得出的判断仍然有效。给出-> *的融合,证明就是对类型派生的简单归纳。

当然,我在这里只介绍了功能核心,但是扩展可以完全模块化。这是对。

术语:: = ... | (x:S)* T | 吨

以琳:: = ... | 头| 尾巴

(s,t:(x:S)* T).head↝s:S

(s,t:(x:S)* T).tail↝t:T [s:S / x]

G⊢*有(x:S)* T-:G⊢*有S; G,z:S⊢*具有T [z / x]

G⊢(x:S)* T有s,t-:G⊢S有s; G⊢T [s:S / x]有t

G⊢e.head是S-:G⊢e是(x:S)* T

G⊢e.tail是T [e.head / x]-:G⊢e是(x:S)* T


1
G, x:S, G' ⊢ x is S -: G' ⊬ x
2015年

1
@ user3237465不。谢谢!固定。(当我用html旋转

1
哦,我以为你只是在指出错字。该规则说,对于上下文中的每个变量,我们综合上下文为其分配的类型。在介绍上下文时,我说:“我们保持不变,即上下文中的变量归因类型是不同的。” 因此不允许遮挡。您会看到,每当规则扩展上下文时,它们总是选择一个新的“ z”,该z实例化我们要使用的任何绑定器。遮盖是恶心。如果您具有上下文x:*,x:x,则本地x的类型不再适用,因为它不在范围内。

1
我只是希望您和其他答复者知道我每次下班休息都会回到这个话题。我真的很想学习这一点,而就我起头的一生来说,我倒是很喜欢它。下一步将是实施和编写一些程序。我很高兴能够生活在这样一个时代,在这个时代,像我这样的人可以在全球范围内获得有关如此精彩的话题的信息,这全都归功于像您这样的天才,他们花了毕生的时间来传播这些知识,免费,在互联网上。再次抱歉对我的问题措辞很不好,谢谢!
MaiaVictor 2015年

1
@cody是的,没有扩展。要了解为什么没有必要,请注意,这两个计算规则使您可以将策略部署到在检查条件之前完全归一化类型的位置,并且还可以在从消除中合成类型之后立即归一化类型。因此,在类型必须匹配的规则中,它们已经被标准化,因此如果“原始”检查和综合类型是可转换的,则在鼻子上相等。同时,由于这个事实,仅将相等性检查限制在该位置是可以的:如果T可转换为规范类型,则将其简化为规范类型。
猪场主'16

8

库里-霍华德的对应关系说,在逻辑上类型系统证明系统之间存在系统的对应关系。以程序员为中心的观点,您可以这样重铸:

  • 逻辑证明系统是编程语言。
  • 这些语言是静态类型的。
  • 这种语言的类型系统的责任是禁止会构成不可靠证据的程序。

从这个角度看:

  • 您总结的未类型化lambda演算没有有效的类型系统,因此建立在其上的证明系统将是不完善的。
  • 简单类型的lambda演算是一种编程语言,具有在句子逻辑中构建声音证明所需的所有类型(“ if / then”,“ and”,“ or”,“ not”)。但是它的类型不足以检查涉及量词的证明(“对于所有x,...”;“存在x使得...”)。
  • 依存类型的Lambda演算具有支持句子逻辑的类型和规则 一阶量词(对值进行量化)的。

这是一阶逻辑的自然演绎规则,使用Wikipedia条目中关于自然演绎的图表。这些基本上也是最小依赖类型的lambda演算的规则。

一阶自然演绎

请注意,规则中包含lambda字词。这些可以理解为构成以其类型表示的句子的证明的程序(或更简单地说,我们只是说程序是证明)。您提供的相似的折减规则可以应用于这些lambda项。


我们为什么要关心这个?好吧,首先,因为证明很可能是编程中的有用工具,并且拥有一门可以与证明打交道的语言作为一流的对象打开了许多途径。例如,如果您的函数具有前提条件,则可以将其证明为参数,而不是将其记录为注释。

其次,因为处理量词所需的类型系统机器可能在编程上下文中有其他用途。特别地,从属类型的语言通过使用称为从属函数类型的概念来处理通用量词(“对于所有x,...”),该函数的结果的静态类型可能取决于参数运行时值

为了提供一个非常实用的应用程序,我一直在编写代码,以便始终读取由具有统一结构的记录组成的Avro文件-所有文件都共享相同的字段名称和类型。这需要我执行以下任一操作:

  1. 将程序中记录的结构硬编码为记录类型。
    • 优点:代码更简单,编译器可以捕获我的代码中的错误
    • 缺点:该程序被硬编码为读取与记录类型一致的文件。
  2. 在运行时读取数据的模式,将其一般表示为数据结构,然后使用该模式来处理记录
    • 优点:我的程序不只被硬编码为一种文件类型
    • 缺点:编译器无法捕获太多错误。

如您在Avro Java教程页面中所见,它们向您展示了如何根据这两种方法使用该库。

使用依赖的函数类型,您可以吃蛋糕,而无需花费更多复杂的类型系统。您可以编写一个函数来读取Avro文件,提取模式,然后以静态类型取决于文件中存储的模式的记录流的形式返回文件的内容。例如,在我尝试访问运行时将要处理的文件的记录中可能不存在的命名字段的情况下,编译器将能够捕获错误。甜甜吧?


1
以您提到的方式在运行时构建类型是我从未想到过的非常酷的事情。确实很甜蜜!感谢您的深刻见解。
MaiaVictor 2015年
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.