Lean4 项目实战指南
Lean4 项目实战
前文介绍了 Lean 开发的三件套工具:elan、lake 和 lean。这些工具的组合类似于其他语言生态中的工具链,比如:
- Rust 中的 rustup + cargo + rustc
- Node.js 中的 nvm + npm + node
也介绍了 Lean 项目的基本结构和文件组织方式。
本文将进行实战演练,完整体验 Lean 项目的开发流程,包括:
- 项目创建
- 依赖管理与配置
- 编写元编程代码
- 测试,自动化以及收录
参考资源
- 元编程示例:MetaExamples
- Lean 中文文档:函数式编程,Lake 文档以及 Lean4 安装指南
准备工作
在开始之前,请确保:
- 已正确安装 Lean4 开发环境,可以在命令行中运行
elan
、lake
和lean
命令 - 了解 Lean 项目的基本结构,熟悉
lakefile.lean
和lakefile.toml
等配置文件的作用
创建项目
首先创建一个新的 Lean 项目,命名为 MyTactics
,并使用 Mathlib 作为依赖库。创建命令如下:
1 | lake new MyTactics math.lean |
执行完成后,将生成如下项目结构:
1 | MyTactics/ |
创建的其他选项
Lake 提供了多种项目创建选项:
1 | # 使用 TOML 格式的依赖配置文件 |
特别注意,当使用 Mathlib 作为依赖时,新建项目会自动使用最新的 Lean 版本,这个行为与执行创建命令时的 Lake 版本无关。需手动修改 lean-toolchain
文件中的版本号。
Lake 项目配置详解
基础配置文件
生成的 lakefile.lean
包含以下主要配置:
1 | import Lake |
配置文件分为三个主要部分:
-
包信息设置(package):
1
2
3
4package "MyTactics" where
leanOptions := #[
⟨`pp.unicode.fun, true⟩
] -
依赖项声明(require):
1
require "leanprover-community" / "mathlib"
-
构建目标设置(lean_lib):
1
2@[default_target]
lean_lib «MyTactics» where@[default_target]
表示这是默认构建目标,使用lake build
时会自动构建。
添加模块
每个 lean_lib
声明都需要对应实际的 Lean 文件,比如添加
1 | lean_lib «Hello» where |
则需在项目目录下添加 Hello.lean
文件。此外,可以用文件夹组织项目代码,例如 Hello/greet.lean
文件通过 import Hello.greet
导入。
修改版本
如果引用了 Mathlib,建议使用以下配置,确保 Mathlib 版本与项目一致,避免构建错误。
1 | require mathlib from git |
这里 s!"v{Lean.versionString}"
会读取仓库当前的 Lean 版本。
配置可执行程序
在 Lean 生态中,很多项目都会提供可执行程序,例如:
lake exe cache get
:用于拉取 Mathlib 的构建缓存lake env jixia
:运行 JiXia 的数据分析工具lake env repl
:启动 Lean 的 REPL 环境
这些可执行命令通过 lakefile.lean
中的 lean_exe
参数进行配置。让我们在项目中添加两个可执行程序的配置:
1 | lean_exe "hello" where |
同样地,被 @[default_target]
标记的目标会在执行 lake build
时自动构建,无需显式执行 lake build demo
。
除了 lean_exe
模块外,也可以用 script
字段定义脚本。例如:
1 | script "greet" (args) do |
运行 lake script greet
命令运行该脚本。
完整配置文件
到这里,我们初始化了一个 Lean 项目,并根据需求修改了依赖。完整的 lakefile.lean
内容如下:
1 | import Lake |
配置完成后的项目结构:
1 | MyTactics/ |
添加了自定义的 Hello/Greet.lean
文件和 Demo.lean
文件。
接下来我们将开始编写具体的实现代码。
项目代码实现
在完成项目配置后,我们开始编写具体的代码实现。本节将分三个部分展示:
- Hello World 程序
- 交互式程序
- 元编程策略
Hello World 程序实现
作为程序员的传统,先从最基础的 Hello World 程序开始。
编写 Hello/Greet.lean
文件:
1 | def name : String := "Rex" |
然后在 Hello.lean
中导入模块:
1 | import Hello.Greet |
可以通过以下几种方式运行程序:
lake exe hello
:重新构建并运行lake env hello
:直接运行已构建的程序lean --run Hello/Greet.lean
:直接执行脚本的main
函数
下图展示了 lake exe
和 lake env
的区别:
💡 关于 Lean 中的副作用
Lean 的计算模型基于表达式求值,理论上是纯函数式的,不存在副作用(side effects)。
但现实中程序需要与外部世界交互,Lean 通过IO
类型优雅地解决了这个问题:
main
函数的类型IO Unit
表明它不是一个直接产生副作用的函数- 而是一个描述了 IO 操作的声明性表达
- 可以将 IO 操作视为一个接收"整个世界"作为输入,并返回新的世界状态的纯函数
更多关于 Lean 函数式编程的讨论,可以参考 Lean 函数式编程指南。
交互式程序
下边演示 Lean 如何处理用户输入。
创建 Demo.lean
文件,实现一个多行文本的拼接程序:
1 | /-- 递归读取用户输入,直到遇到空行为止 -/ |
这里 partial
关键字用于声明递归函数 getLines
,告诉 Lean 编译器不需要验证函数的终止性。执行效果:
Lean 社区的 REPL 就是基于类似的机制实现 Lean 与外部环境的信息交流。
编写元策略
在自定义元策略之前,先了解两个常用的 tactic:
repeat
:重复执行某个策略直到证明完成first | tac₁ | tac₂ | ...
:按顺序依此尝试策略,任一成功即停止,否则报错
通过一道证明题来说明这些策略的用法:
1 | -- 逐步证明 2 ≤ 6 |
repeat
这一长串显然不太优雅,为了简化类似的证明过程,我们定义一个元策略:
1 | /-- |
将内容写入 MyTactics/Basic.lean
文件,并在 MyTactics.lean
中导入:
1 | import MyTactics.Basic |
之后通过 import MyTactics
即可在其他文件中使用该策略。
使用示例
现在,前边的例子可以这样写:
1 | example : 2 ≤ 6 := by |
或者用于证明其他类似式子:
1 | example : 30 < 39 := by |
小结
我们演示了三个例子:
- Hello World 程序:
Hello/Greet.lean
- 如何处理用户输入:
Demo.lean
- 元策略示例:
MyTactics/Basic.lean
完整代码可以在仓库中找到:Lean-zh/MyTactics。
测试,自动化及发布
测试配置
最基本的测试配置是在 lakefile.lean
中使用 @[test_driver]
标记测试模块。
比如标记 lean_exe
模块:
1 | @[test_driver] |
然后在 Tests/Main.lean
中实现测试逻辑:
1 | def main : IO Unit := do |
使用 lake test
命令运行所有标记为测试的模块。
此外,也可以直接编写 shell 脚本,REPL 项目就是这么做的。当然,也有用于 Lean 测试的项目 LSpec,后续再进一步探索。
自动化
自动化方面,这里贴一个 GitHub Workflow 供参考。当向 main
分支提交 PR 或者推送代码时触发。
1 | name: Run Tests |
项目发布
目前 Lean 生态系统还没有类似 Python 的 PyPI 这样的包注册表,无法通过类似 pip install
的方式直接安装包。Lean 项目主要通过 GitHub 托管和分发,使用 git 方式拉取依赖。
但是社区有个 Reservoir 网站 作为包索引平台,提供包的集中展示、构建测试和兼容性验证。收录项目需满足:
- 公开的原创 GitHub 仓库(不接受 fork 或模板生成)
- 根目录包含
lake-manifest.json
文件 - 使用 OSI 认证的开源许可证
- GitHub 仓库至少获得 2 个星标
如果项目满足这些条件,可以通过在 Reservoir 仓库提交 issue 申请收录,增加项目在社区的曝光度。
以上,欢迎讨论交流~