面向程序员的Lean 4教程(3) - 类型是一等公民
有了前面像传统编程语言一样使用Lean 4的经验后,我们引入一点Lean 4中与传统语言不太一样的东西:类型。
比如,在Lean 4中,类型如Bool, Nat, Int本身也是一级对象,可以绑定到变量中,它们的类型是Type.
Type类型
我们来看将类型做为一等公民的用法:
def t1 : Type := Nat
def t3 : Type := Bool
def t4 : Type := UInt16
除了基本类型是Type类型,我们还可以看到下面的类型:
def t2 : Type := Nat → Nat
def t8 := Nat → Nat → Nat
我们可以使用#check
命令来查看一个变量的类型:
#check t1
#check t2
#check t3
#check t4
#check t8
输出如下:
info: .\.\.\.\Test3\Basic.lean:21:0: t1 : Type
info: .\.\.\.\Test3\Basic.lean:23:0: t2 : Type
info: .\.\.\.\Test3\Basic.lean:25:0: t3 : Type
info: .\.\.\.\Test3\Basic.lean:27:0: t4 : Type
info: .\.\.\.\Test3\Basic.lean:35:0: t8 : Type
聪明的你可能会想到,既然Type类的对象是一等公民,那么Type本身是不是也是一等公民呢?
恭喜你,你发现了Girard 悖论,类似于集合论中的罗素悖论,一个集合不能包含自身,一个类型也不能是超越自己的类型。
在Lean 4中,通过类型的层次来解决这个问题。
Type的变量的类型是Type 1:
def t5 : Type 1:= Type
同样,Type 1变量的类型是Type 2:
def t6: Type 2 := Type 1
同理,Type 1 → Type
的类型也是Type 2:
def t7: Type 2 := Type 1 → Type
打印出来看看:
#check t5
#check t6
#check t7
结果如下:
info: .\.\.\.\Test3\Basic.lean:29:0: t5 : Type 1
info: .\.\.\.\Test3\Basic.lean:31:0: t6 : Type 2
info: .\.\.\.\Test3\Basic.lean:33:0: t7 : Type 2
函数的类型
同类型一样,Lean 4中的函数也是一等公民。那么,函数是什么类型呢?
我们看下下面4个函数:
def f1 := fun x => x+(1:Int)
def f2 := λ x => x+(2:Int)
def f3 := (. + (3:Int))
def f4 (x : Int) : Int :=
x+4
其中,最后的f4是传统语言最熟悉的函数。
f1也是现代语言中普遍支持的lambda函数的常见表达形式。f2不伪装了,直接就写λ表达式。
我们检查下它们的类型:
#check f1
#check f2
#check f3
#check f4
运行结果如下:
info: .\.\.\.\Test3\Basic.lean:88:0: f1 (x : Int) : Int
info: .\.\.\.\Test3\Basic.lean:90:0: f2 (x : Int) : Int
info: .\.\.\.\Test3\Basic.lean:92:0: f3 : Int → Int
info: .\.\.\.\Test3\Basic.lean:94:0: f4 (x : Int) : Int
其实它们的类型都是Int → Int
, 它也是Type类的实例。
归纳类型
类似于古老的枚举类型,归纳类型在现代语言中也越来越流行。
归纳类型是将类型中所有可能的值都一一列举出来,最简单的就像布尔类型,比如我们定义一个Bool2类型:
inductive Bool2 : Type where
| true2 : Bool2
| false2 : Bool2
def b1 : Bool2 := Bool2.true2
def b2 : Bool2 := Bool2.false2
#check b1
#check b2
我们可以给Bool2写一个取反函数:
def not2 : Bool2 → Bool2
| Bool2.true2 => Bool2.false2
| Bool2.false2 => Bool2.true2
这个函数我们直接取Bool2 → Bool2
作为类型。
或者我们懒得写类型,就直接写λ\lambdaλ表达式,类型让Lean4自己去推断:
def not2 := λ
| Bool2.true2 => Bool2.false2
| Bool2.false2 => Bool2.true2
#check not2
基本类型的真面目
这一节我们学习一个新的命令#print
, 通过它我们可以查看到之前学习的类型的真面目。
比如我们看看Bool类型是个什么:
#print Bool
输出如下:
info: .\.\.\.\Test3\Basic.lean:41:0: inductive Bool : Type
number of parameters: 0
constructors:
Bool.false : Bool
Bool.true : Bool
原来Bool类型就是由false和true归纳出来的类型。
我们再看看Nat是个什么:
info: .\.\.\.\Test3\Basic.lean:39:0: inductive Nat : Type
number of parameters: 0
constructors:
Nat.zero : Nat
Nat.succ : Nat → Nat
原来Nat也是个归纳类型,由0和后继函数两者组成。
我们来用归纳类的方式来使用Nat:
def chkNat : IO Unit := do
let n1 := Nat.zero
let n2 := n1.succ
IO.println n1
IO.println n2
输出为0和1.
我们再看看Int:
info: .\.\.\.\Test3\Basic.lean:43:0: inductive Int : Type
number of parameters: 0
constructors:
Int.ofNat : Nat → Int
Int.negSucc : Nat → Int
原来堂堂Int类型是两个自然数到整数的转换函数的归纳。
我们来操练一下:
def chkInt : IO Unit := do
let i1 : Int := Int.ofNat Nat.zero.succ
let i2 : Int := Int.negSucc Nat.zero
IO.println i1
IO.println i2
输出为1和-1。
再来看一个复杂的一点的,List:
#print List
输出如下:
info: .\.\.\.\Test3\Basic.lean:57:0: inductive List.{u} : Type u → Type u
number of parameters: 1
constructors:
List.nil : {α : Type u} → List α
List.cons : {α : Type u} → α → List α → List α
类似于自然数的定义,列表由空列表和拼接列表的函数两个部分组成。
列表需要指定类型,这在普遍支持泛型的时代也不是什么新鲜事。
我们用这两个基本函数来构造列表:
def chkList : IO Unit := do
let l1 : List Nat := List.nil
let l2 : List Nat :=List.cons 1 l1
let l3 : List Nat :=List.cons 2 l2
IO.println l1
IO.println l2
IO.println l3
输出如下:
[]
[1]
[2, 1]
类型依赖于变量
如果类型只依赖于变量的类型,就像其他语言中的泛型,大家都很熟悉。
如果类型依赖于变量的值,那么对于很多同学来说,可能就是超出预期了。
其实,因为类型是一等公民,它们其实都是Type类的实例,所以简单的根据变量的值来决定类型,只是改变Type类的实例的值而已。
比如我们有一个类型,依赖的变量如果是0,那么类型是Int;其他情况下类型是String。
-- 定义一个依赖类型,根据变量的值决定类型
def TypeDependingOnValue (n : Nat) : Type :=
if n = 0 then Int else String
-- 示例使用
def example1 : TypeDependingOnValue 0 := (0 : Int) -- 类型为 Int
def example2 : TypeDependingOnValue 1 := "Hello" -- 类型为 String
-- 打印结果
#eval example1 -- 输出: 0
#eval example2 -- 输出: "Hello"
这样看起来跟其他语言的代码也没有什么本质区别,是吧?
下面我们再把上面的代码改的更实用一点。比如你现在当助教,要录入学生的考试成绩。如果缺考的话,写成0分或者-1分都不优雅。我们可以根据考生的考试状态来决定分数的类型,就跟我们上面的例子类似。参加考试的就录分数,就是Nat类型,缺考的就录成String类型。
-- 定义学生是否参加考试的类型
inductive ExamStatus
| attended
| notAttended
-- 定义分数类型,依赖于学生是否参加考试
def Score (status : ExamStatus) : Type :=
match status with
| ExamStatus.attended => Nat
| ExamStatus.notAttended => String
-- 定义依赖对类型,表示学生及其分数
def StudentWithScore : Type := Σ (status : ExamStatus), Score status
-- 定义一个学生参加了考试,分数为85
def student1 : StudentWithScore := ⟨ExamStatus.attended, (85:Nat)⟩
#check student1
#print student1
-- 定义一个学生没有参加考试,分数为"未参加"
def student2 : StudentWithScore := ⟨ExamStatus.notAttended, "未参加"⟩
#check student2
#print student2
我们再来一个更高级的,我们想打印变量的值,但是需要这个变量支持ToString实例。
我们可以写出这样一个东西:
def examplePi : ∀ (α : Type) [inst : ToString α], α → String
| _, _, x => s!"The input is: {x}"
#check examplePi
#print examplePi
如果这种看起来不太习惯,我们可以换成另一个写法:
def toStringPi : ∀ (α : Type) [inst : ToString α], α → String :=
λ α inst x => s!"The input is: {x}"
#check toStringPi
#print toStringPi
最后这一小节暂时还不理解也没关系,我们只是举例说明类型依赖可以做很多传统泛型做不到的事情。
小练习
用归纳类型定义一个五行的类,成员是木火土金水。
然后写两个函数,输出它的相生相克。
比如,木生火,火生土,土生金,金生水,水生木。
水克火,火克金,金克木,木克土,土克水。
最后写几个测试用例,验证一下。
参考例子:
-- 定义五行的类型
inductive WuXing : Type
| metal -- 金
| wood -- 木
| water -- 水
| fire -- 火
| earth -- 土
open WuXing
-- 定义相生关系
def generates : WuXing → WuXing → Prop
| water, wood => true -- 水生木
| wood, fire => true -- 木生火
| fire, earth => true -- 火生土
| earth, metal => true -- 土生金
| metal, water => true -- 金生水
| _, _ => false
-- 定义相克关系
def overcomes : WuXing → WuXing → Prop
| water, fire => true -- 水克火
| fire, metal => true -- 火克金
| metal, wood => true -- 金克木
| wood, earth => true -- 木克土
| earth, water => true -- 土克水
| _, _ => false
-- 示例:检查五行之间的相生和相克关系
example : generates water wood := by simp [generates]
example : overcomes water fire := by simp [overcomes]
example : ¬ generates water fire := by simp [generates]
example : ¬ overcomes water wood := by simp [overcomes]
虽然用了一些没讲到的语法,但是这个例子应该不难理解,就像写测试用例一样,没有什么复杂的。
小结
这一节我们学习了类型作为一等公民的用法,包括类型依赖于变量,类型依赖于值,类型依赖于函数等。
我们没有讲Π\PiΠ类型依赖和Σ\SigmaΣ类型依赖之类的理论,我们也没有讲数学证明。我们只要理解类型是一等公民就可以了。