我们已经掌握了直觉逻辑(Intuitionistic Logic,IL),
------------------------------------------------------------------------------------
PS:这一小段是笔者扩充说明下:直觉逻辑。总所周知,最为著名的“经典逻辑(classical logic)/一阶逻辑”是学习逻辑的一个起点,以及其他逻辑作为参照的标杆。经典逻辑侧重“真值”,陈述的“真值”是其“绝对”特征。一个无歧义的合式陈述(well-formed statement)或真或假。假即非真,真即非假,是为“排中律”。它强调排中律。例如:基于经典逻辑,我们可以“非构造地”证明一个命题。如下图:
上面的证明虽然在经典逻辑里没有问题,但我们仍无法确定究竟哪一种情况是正确的。除此之外,我们还可以做出一个构造性证明(constructive proof):对于 我们有 。这种“构造式”的推理方式对应着“直觉主义逻辑”(intuitionistic logic)。直觉主义逻辑的哲学基础是,不存在绝对真理,只存在理想化数学家(创造主体)的知识和直觉主义构建。逻辑判断为真当且仅当创造主题可以核实它。所以,直觉主义逻辑不接受排中律。
直觉主义命题逻辑,或称直觉主义命题演算(Intuitionistic propositional calculus, IPC),的语言和经典命题逻辑的语言是一样的。直觉主义逻辑里的语义不是通过真值表来判断的,而是通过构建模式来解释的。这就是著名的BHK释义(Brouwer-Heyting-Kolmogorov interpretation)。Heyting 是 Brouwer的学生。Kolmogorov有个著名的学生叫Martin-Löf。如下:
有很多逻辑式在经典逻辑里是重言式(tautology),但在直觉主义逻辑里却没有构造。如常见的排中律、双重否定消除、De Morgan定律、反证法等等。直觉主义逻辑的一个证明系统是自然演绎系统 ,其中J代表直觉主义逻辑。NK是经典逻辑的自然演绎系统。
布尔代数是经典逻辑的一种语义诠释,而海廷代数则是直觉主义逻辑的代数语义学。在直觉主义逻辑里,我们关注的首要问题是“可证明性”(provability),而非“真值”。 详情除了上述的链接外,还可以参考:直觉主义逻辑
------------------------------------------------------------------------------------
我们再回到lambda演算:我们已经得到了我们需要定义模型的逻辑工具。 当然,在没有更简单的事情了,对吧?
到目前为止我们讨论的都是简单的无类型lambda演算。一如丘奇首次提出LC的第一个版本。但它存在一些问题,为了解决这些问题,人们引入了「类型」(type)的概念,于是出现了简单类型lambda演算,之后出现了各种变种 —— SystemT,SystemF,Lambda立方(和时间立方没啥关系:-))等等。最终,人们意识到无类型lambda演算实际上是类型化lambda演算的一个简单到病态的特例 —— 只有一个类型的LC。
lambda演算的语义在类型化演算中最容易理解。现在,我们来看看最简单的类型化LC,叫做「简单类型化lambda演算」(simply typed lambda calculus);以及它如何在语义上等同于直觉逻辑。(其实上,每个种类型化LC都对应于一种IL,而且每个LC中的beta规约都对应于IL中的一步推理,这就是为什么我们需要先跑去介绍直觉逻辑,然后再回到这里。)
类型化lambda演算的主要变化是增加了一个叫做「基类型」(base types)的概念。在类型化lambda演算中,你可以使用一些由原子值构成的论域(universe), 这些值分为不同的简单类型。基类型通常由单个的小写希腊字母命名,然而这正好是Blogger的痛处(普通html文本打不出希腊字母),我只好用大写英文字母来代替类型名称。因此,例如,我们可以有一个类型「N」,它由包含了自然数集合,也可以有一个类型「B」,对应布尔值true / false,以及一个对应于字符串类型的类「S」。
现在我们有了基本类型,接下来我们讨论函数的类型。函数将一种类型(参数的类型)的值映射到的第二种类型(返回值的类型)的值。对于一个接受类型A的输入参数,并且返回类型B的值的函数,我们将它的类型写为A -> B 。「 ->」叫做函数类型构造器(function type constructor),它是右关联的,所以 A -> B -> C 表示 A -> (B -> C)。
为了将类型应用于lambda演算,我们还要做几件事情。首先,我们需要更新语法,使我们可以包含类型信息。第二,我们需要添加一套规则,以表示哪些类型化程序是合法的。
语法部分很简单。我们添加了一个「:」符号; 冒号左侧是表达式或变量的绑定,其右侧是类型规范。 它表明,其左侧拥有其右侧指定的类型。举几个例子:
lambda x : N . x + 3
。表示参数x
类型为N
,即自然数。这里没有指明函数的结果的类型;但我们知道,函数「+」的类型是 N -> N
,于是可以推断,函数结果的类型是N。(lambda x . x + 3) : N -> N
,这和上面一样,但类型声明被提了出来,所以它给出了lambda表达式作为一个整体的类型。这一次我们可以推出 x : N
,因为该函数的类型为 N -> N
,这意味着该函数参数的类型为 N 。lambda x : N, y : B . if y then x * x else x
。这是个两个参数的函数,第一个参数类型是 N ,第二个的类型是 B 。我们可以推断返回类型为 N 。于是整个函数的类型是 N -> B -> N
。乍看之下有点奇怪;但请记住,lambda演算实际上只有单个参数;多参数函数的写法只是柯里化的简写。所以实际上这个函数是:lambda x : N . (lambda y : B . if y then x * x else x)
;内层lambda的类型是 B -> N
; 外层类型是 N -> (B -> N)
。为了讨论程序是否关于类型合法(即「良类型的」(well-typed) ),我们需要引入一套类型推理规则。当使用这些规则推理一个表达式的类型时,我们称之为类型判断(type judgement)。类型推理和判断使我们能推断lambda表达式的类型;如果表达式的任一部分和类型判断结果不一致,则表达式非法。(丘奇开始研究类型化LC的动机之一是区分「原子」值和「谓词」值,他通过使用类型以确保谓词不能操作谓词,以试图避免的哥德尔式的悖论。)
我将采用一套不太正规的符号表示类型判断;标准符号太难用我目前使用的软件渲染了。常用的符号跟分数有点像;分子由我们已知为真的语句组成;分母则是我们可以从分子中推断出来的东西。 我们经常在分子中使用一个叫「上下文」(context)的概念,它包含了一组我们已知为真的语句,通常表示为一个大写的希腊字母。这里我用大写的希腊字母的名称表示。如果一个类型上下文包含声明”x : A
,我会写成 CONTEXT |- x : A
。对于分数形式的推理符号,我用两行表示,分子一行标有「Given: 」,分母一行标有「Infer: 」。 (正常符号用法可以访问维基百科的STLC页 。)
**规则1:(类型标识) **
Given: nothing
Infer: x : A |- x : A
最简单的规则:如果我们只知道变量的类型声明,那么我们知道这个变量是它所声明的类型。
**规则2:(类型不变式) **
Given: GAMMA |- x : A, x != y
Infer: (GAMMA + y : B) |- x : A
这是不干涉语句。 如果我们知道 x : A
,那么我们可以推断出其他任何类型判断都不能改变我们对x
的类型推断。
规则3:(参数到函数的推理)
Given: (GAMMA + x : A) |- y : B
Infer: GAMMA |- (lambda x : A . y) : A -> B
这个语句使我们能够推断函数的类型:如果我们知道函数参数的类型是 A
,而且该函数返回值的类型是 B
,那么我们可以推出函数的类型为 A -> B
。
最后,Rule4:(函数应用推理)
Given: GAMMA |- x : A -> B, GAMMA |- y : A
Infer: GAMMA |- (x y) : B
如果我们知道一个函数的类型为 A -> B
,且把它应用到类型为A
的值上,那么结果是类型为 B
的表达式。
规则就是这四个。如果我们有一个lambda表达式,且表达式中每一项的类型判断都保持一致,那么表达式就是良类型化的(well-typed)。如果不是,则表达式非法。
下面我们找点刺激,描述下SKI组合子的类型。这些都是不完整的类型——我用的是类型变量,而不是具体的类型。 在真正使用组合子的程序中,你可以找到实际类型来替换类型变量。 别担心,我会用一个例子来阐明这一点。
I
组合子: (lambda x . x) : A -> A
K
组合子: (lambda x : A . ((lambda y : B . x) : B -> A)): A -> B -> A
S
组合子: (lambda x : A -> B-> C . (lambda y : A -> B . (lambda z : A . (x z : B -> C) (y z : B)))) : (A -> B -> C) -> (A -> B) -> C
现在,让我们来看一个简单的lambda演算表达式:lambda x y . y x
。由于没有任何关于类型的声明或参数,我们无法知道确切的类型。但是,我们知道,x
一定具有某种类型,我们称之为A
;而且我们知道,y
是一个函数,它以x
作为应用的参数,所以它的参数类型为A
,但它的结果类型是未知的。因此,利用类型变量,我们有 x : A, y : A -> B
。我们可以通过看分析完整的具体表达式来确定A
和 B
。所以,让我们用x = 3
,和y = lambda a : N. a * a
来计算类型。假设我们的类型上下文已经包含了 *
的类型为 “N -> N -> N
“。
(lambda x y . y x) 3 (lambda a : N . a * a)
3 : N
。a * a
的类型是 N
,其中 a : N
(*
的类型:N -> N -> N
),因此,由规则3,lambda表达式的类型是 N - > N
。 于是,我们的表达式现在变成了:(lambda x y . y x) (3 : N) (lambda a : N . (a * a) : N) : N -> N
x
须是 N
类型,以及 y
是 N -> N
类型 。根据规则4我们知道,应用表达式的类型 y x
一定是 N
,然后根据规则3,表达式的类型为: N -> (N -> N) -> N
。N
。所以,现在我们得到了一个简单的类型化lambda演算。说它是简单的类型化,是因为这里对类型的处理方式很少:建立新类型的唯一途径就是通过「 ->
」 构造器。其他的类型化lambda演算包括了定义「参数化类型」(parametric types)的能力,它将类型表示为不同类型的函数。
我们已经讲过直觉逻辑(intuitionistic logic)和它的模型;从无类型的Lambda演算讲到了简单类型化Lambda演算;终于,我们可以看看Lambda演算模型了。而这正是真正有趣的地方。
先来考虑简单类型化Lambda演算中的类型。任何可以从下面语法生成的形式都是Lambda演算类型:
type ::= primitive | function | ( type )
primitive ::= A | B | C | D | ...
function ::= type -> type
这个语法中的一个陷阱是,你可以创建一个类型的表达式,而且它们是合法的类型定义,但是你无法你写出一个拥有该类型的单独的,完整的,封闭表达式。(封闭表达式是指没有自由变量的表达式。)如果一个表达式类型有类型,我们说表达式「居留」(inhabit)该类型,而该类型是一个居留类型。如果没有表达式可以居留类型,我们说这是「不可居留的」(uninhabitable) 。
那么什么是居留类型和不可居留类型之间的区别?
答案来自一种叫做「柯里-霍华德同构」(Curry-Howard isomorphism)的理论。这种理论提出,每个类型化的lambda演算,都有相应的直觉逻辑;类型表达式是可居留的当且仅当该类型是在对应逻辑上的定理。
先看类型 A -> A
。现在,我们不把 ->
看作函数类型构造器,而把它视作逻辑蕴涵。A 蕴含 A
显然是直觉主义逻辑的定理。因此,类型 A -> A
是可居留的。
再来看看 A -> B
。这不是一个定理,除非在某个上下文中能证明它。作为一个函数类型,这表示一类函数,在不包括任何上下文的情况下,以A类型作为参数,并返回一个不同类型B。你没法做到这一点——必须有某个上下文提供B类型的值——为了访问这个上下文,必须存在某种允许函数访问它的上下文的方式:一个自由变量。这一点在逻辑上和lambda演算上是一样的:你需要某种上下文建立 A->B
作为一个定理(在逻辑上)或可居留的类型(在lambda演算上)。
下面就容易理解些了。如果有一个封闭LC表达式,其类型是在相应的直觉逻辑中的定理,那么,该类型的表达式就是定理的一个证明。每个Beta规约则等同于逻辑中的一个推理步骤。对应于这个lambda演算的逻辑就是它的模型。从某种意义上说,lambda演算和直觉逻辑,只是同一件事的不同反映。
有两种方式可以证明这个同构:一种是柯里当初采用的,组合子演算的方式;另一种则用到了所谓的「相继式演算」(Sequent calculus)。我会组合子证明的版本,所以下面我会快速的过一遍。以后,很可能下个礼拜,我会讲相继式演算的版本。
让我们回忆一下什么是模型。模型是一种表示演算中的每条声明(statement)在某些具体值域上都合法的方式——所以存在具体实体和演算中的实体的对应关系,凡演算中的声明都对应真正的实体的某些声明。所以我们实际上并不需要做充分的同构;我们只需要一个从演算到逻辑的同态(homomorphism)。(同构是双向的,从演算到逻辑和逻辑到演算;而同态只从演算到逻辑。)
所以我们需要做的是取任意完整的lambda演算表达式,然后将其转化为一系列合法的的直觉逻辑语句。由于直觉逻辑本身已被证明是合法的,如果我们可以把lambda演算翻译成IL,这样我们就证明了lambda演算的合法性——这意味着我们将表明,在lambda演算中的计算是合法的计算,以及lambda演算是一个完全的,合法的,有效的计算系统。
我们如何从组合子(它们只是省去了变量的lambda演算的简写)得到直觉逻辑?它实际上简单得令人难以相信。
直觉逻辑中的所有证明可以归结为一系列的步骤,其中的每一步都是使用了以下两个基本公理之一的推理:
A implies B implies A
(A implies B implies C) implies ((A implies B) implies (A implies C))
让我们用箭头重写它们,让它们看起来像一个类型:A -> B -> A
;及(A -> B -> C) -> ((A -> B) -> (A -> C))
。
眼熟吗?不熟的话再回头看看简单类型化lambda演算。这就是S和K组合子的类型。
接下来的建模步骤就很明显了。lambda演算的类型对应于直觉逻辑的原子类型。函数是推理规则。每个函数可以规约为一个组合子表达式;每个组合子表达式是直觉逻辑的某个基本推理规则的实例。于是,函数就成了相应逻辑里的定理的一个构造性证明。
酷吧?
(任何正常人看完会说“什么?”,但,我显然不是正常人,我是一个数学怪咖。)