面向程序员的Lean 4教程(3) - 类型是一等公民

面向程序员的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 := NatNat
def t8 := NatNatNat

我们可以使用#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 1Type

打印出来看看:

#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 : Bool2Bool2
  | 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 : WuXingWuXingProp
  | water, wood   => true  -- 水生木
  | wood,  fire   => true  -- 木生火
  | fire,  earth  => true  -- 火生土
  | earth, metal  => true  -- 土生金
  | metal, water  => true  -- 金生水
  | _,     _      => false

-- 定义相克关系
def overcomes : WuXingWuXingProp
  | 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Σ类型依赖之类的理论,我们也没有讲数学证明。我们只要理解类型是一等公民就可以了。

<think>好的,我现在要解决Lean4构建过程中遇到的&#39;no-build --no-cache failed&#39;错误。首先,我需要弄清楚这个错误的具体原因。根据用户提供的引用信息,可能存在几个潜在的问题点。 首先看引用[1],用户在安装GCC-4.8.5时遇到了配置错误,尽管后续步骤是否成功完成并未明确。如果配置阶段就有错误,可能会影响后续的编译过程。不过用户提到的是Lean4的问题,可能与GCC的安装有关联,但需要确认Lean4是否依赖特定版本的GCC或其他编译器。 引用[2]提到了修改protobuf的路径,这说明项目中可能涉及protobuf库的配置。如果路径设置不正确,可能导致构建过程中找不到必要的头文件或库文件,从而引发错误。 引用[3]是关于创建新Lean4项目的命令,用户可能在尝试新建项目时遇到了问题。Lake工具的使用是否正确,项目结构是否符合要求,都可能影响构建结果。需要检查命令是否正确执行,以及项目配置是否合理。 引用[4]中的错误是关于找不到&#39;mfxvideo.h&#39;头文件,这通常是由于缺少相关开发库或路径配置错误导致的。虽然这是另一个项目中的问题,但同样提示了依赖项安装和路径设置的重要性。 结合这些信息,我推测&#39;no-build --no-cache failed&#39;错误可能与以下几个方面有关: 1. **依赖项安装不完整或配置错误**:比如GCC版本不兼容、protobuf路径不正确,或者缺少其他必要的开发库。 2. **构建工具配置问题**:Lake工具的配置可能有误,导致无法正确下载依赖或编译项目。 3. **环境变量设置错误**:例如路径设置不正确,导致编译器或构建工具找不到必要的文件。 4. **缓存或旧文件干扰**:使用--no-cache参数时,可能旧有的缓存文件影响了构建过程,但强制不使用缓存后,某些步骤无法完成。 接下来,我需要按照这些可能性逐一排查,找到具体原因并给出解决方案。首先,检查依赖项是否正确安装并配置,尤其是protobuf和GCC的版本是否符合Lean4的要求。其次,验证Lake工具的配置命令是否正确,项目结构是否完整。然后,查看环境变量,特别是涉及库路径和编译器路径的设置是否正确。最后,尝试清理项目并重新构建,排除缓存或残留文件的干扰。 在此过程中,可能需要查看构建日志的详细信息,定位具体的错误位置和原因。例如,是否有编译错误、链接错误,或是文件缺失等提示。根据错误信息进一步调整配置或安装缺失的组件。</think>针对Lean4构建过程中遇到的&#39;no-build --no-cache failed&#39;错误,以下是逐步排查与解决方案: --- ### **1. 检查依赖项安装与配置** - **GCC版本兼容性** Lean4可能依赖特定版本的编译器。根据引用[1],用户安装的是GCC-4.8.5。建议升级至更高版本(如GCC 11+),并确保编译Lean4时使用的GCC版本符合[官方要求](https://leanprover.github.io/lean4/doc/setup.html#platform-support)。 ```bash # 示例:更新GCC(Ubuntu) sudo apt-get install gcc-11 g++-11 sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-11 110 sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-11 110 ``` - **Protobuf路径设置** 引用[2]中提到修改`protobuf`路径。确保路径指向正确的Protobuf安装目录,并在项目配置文件(如`leanpkg.toml`)中正确引用: ```toml [dependencies] protobuf = { path = "/home/jarvis/protobuf" } ``` --- ### **2. 验证Lake工具配置** - **创建新项目** 使用引用[3]中的命令创建新项目,确保Lake版本兼容。如果项目初始化失败,尝试更新Lake工具: ```bash # 更新Lake lake update # 创建新项目 lake +leanprover-community/mathlib4:lean-toolchain new my_project math ``` - **检查项目结构** 确认项目目录包含`lean-toolchain`文件,内容指向正确的Lean4版本(如`leanprover/lean4:nightly-2023-02-10`)。 --- ### **3. 配置环境变量** - **设置路径变量** 确保`PATH`包含Lean4、GCC和Protobuf的二进制路径: ```bash export PATH="/path/to/lean4/bin:$PATH" export PATH="/path/to/gcc/bin:$PATH" export PATH="/path/to/protobuf/bin:$PATH" ``` - **检查库路径** 若出现头文件或库缺失(如引用[4]中的`mfxvideo.h`错误),设置`C_INCLUDE_PATH`和`LIBRARY_PATH`: ```bash export C_INCLUDE_PATH="/path/to/include:$C_INCLUDE_PATH" export LIBRARY_PATH="/path/to/lib:$LIBRARY_PATH" ``` --- ### **4. 清理并重新构建** - **强制清理缓存** 使用`--no-cache`参数可能跳过必要步骤,建议先完整清理项目: ```bash lake clean rm -rf .lake/build # 手动删除构建目录 ``` - **重新构建** 指定详细日志以定位错误: ```bash lake build -v 2>&1 | tee build.log ``` --- ### **5. 分析构建日志** 查看`build.log`中的关键错误信息: - **编译错误**:检查缺失头文件或语法错误,确认依赖项版本。 - **链接错误**:确认库路径正确,静态库(`.a`)存在。 - **权限问题**:确保构建目录可写。 --- ### **示例解决方案** 假设日志显示`error: &#39;protobuf/descriptor.h&#39; not found`: 1. 确认Protobuf已安装且路径正确。 2. 在项目配置中添加路径: ```toml [package] extraIncludeDirs = ["/home/jarvis/protobuf/include"] extraLibDirs = ["/home/jarvis/protobuf/lib"] ``` 3. 重新构建。 --- ### **相关问题** 1. 如何更新Lean4的Mathlib依赖? 2. Lake工具常见配置错误有哪些? 3. 如何在Windows系统上配置Lean4开发环境? --- 通过以上步骤,应能解决大部分由依赖配置或环境问题导致的&#39;no-build --no-cache failed&#39;错误。若问题持续,建议提供完整的构建日志以便进一步分析。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jtag特工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值