Skip to content
/ TinyLean Public
generated from pypa/sampleproject

Tiny theorem prover with syntax like Lean 4 in <1K LOC

License

Notifications You must be signed in to change notification settings

anqur/TinyLean

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TinyLean

Supported Python versions Lines of Python Test codecov

仅用不到 1K 行 Python 实现的 Lean 4 风格定理证明器。

def TinyLean ≔ {T: Type} → (a: T) → T
  • 你可以在这个项目中学到基础的定理证明(theorem proving)原理及其实现。
  • 丰富的中文注释,并以工业界常用词汇的使用优先,帮助你轻松地将工业及学术用语联系在一起。
  • 丰富的单元测试和高覆盖率,帮助你在做出任何修改时执行回归测试,或改写成其他语言项目时当做参考。

Note

In pre-v1 releases I used English everywhere in this project for convenience. So I feel very sorry for the early stargazers who might not expect full Chinese content here (including documentation and comments), since I decided to retarget the audience. Please reach me if you have trouble of any kind.

❓ 疑问

“这个定理证明有多‘强’?”

如果你了解甚至十分熟悉 PLT(编程语言理论)的知识,这个项目实现了以下语言特性:

点击展开剧透
  • Dependently-typed lambda calculus
  • Holes/goals
  • Implicit arguments (no first-class polymorphism)
  • Inductive data type (à la pi-forall)
  • Dependent pattern matching (à la pi-forall)
  • Typeclass (no chained instances)

TinyLean 可能是你能找得到的以上特性结合一块的最短实现。在本文末尾的探索部分你还能找到更多资料。

“我对定理证明不感兴趣,所以它还能做什么?”

当你有一个比 C++、Java、TypeScript、Rust、Haskell 强大的类型系统,它的实现不到 1K 行,还适合移植到其他的语言时,再仔细思考一下你想拿它做些什么事情。

我用这些知识实现了 RowScript 编程语言,一个支持行多态(row polymorphism)的 JavaScript 方言。

“项目名为什么是 TinyLean?为什么项目名不是 mini-lean,μLean 之类的其他名字呢?”

“TinyLean”是对 TinyIdris 项目的致敬,TinyIdris 是一个“极简” Idris 编程语言的实现。

“为什么使用中文?”

即使使用英文,PLT 领域内就连最简单的术语都充满着歧义和晦涩。如果你对 PLT 里的各种术语仍未祛魅,去搞清楚 dependent sum typesum type 的区别吧,这是每一个 PLer 学习过程中必吃的 💩。

我会用尽可能简单、常见的语言和术语,来帮助你祛魅的过程。

⏬ 安装

尝试

如果只是想尝试玩玩本项目,可以从 PyPI 上安装完整的实现:

pip install TinyLean

tinylean 命令执行任意 .lean 文件:

tinylean example.lean

甚至可以执行一个 Markdown 文件,所有标记了 ```lean 的代码块都会执行类型检查。

Important

你正在阅读的 README 文件是个合法的 TinyLean 文件!

tinylean example.md

本地阅读源码

克隆本项目:

git clone https://github.com/anqurvanillapy/TinyLean
cd TinyLean/

本地测试任何你的 .lean/.md 文件,比如本文件:

python -m src.TinyLean .github/README.md

安装并使用 pytest 执行所有单元测试:

pip install pytest
pytest

🧙指南

那么,欢迎来到定理证明的世界!让我们一步步实现如何优雅的证明旷世难题 1+1=2

DTLC

一开始,这个世界仅有这些东西:

  • 类型的类型(type of type,也叫 universe),即 Type
  • 引用,又叫变量名,形如 x
  • 函数,形如 λ x y ↦ y
  • 函数类型,形如 (x: Type) → (y: Type) → Type
  • 调用,形如 x y

这个世界有个名字,叫 DTLC(dependently-typed lambda calculus)。

做一些简单的 lambda 演算,比如定义一个叫 id 的恒等函数(identity function),接受一个 a 并返回去:

def id (T: Type) (a: T): T := a

def Hello: Type := Type

example := id Type Hello

ITP 与 ATP

定理证明器的功能往往都是交互式的(ITP,interactive theorem proving),这的意思是,当你不太清楚目前证明所需要的信息时,你可以询问证明器,例如:

def myLemma
  (a: Type)
  (b: (_: Type) -> Type)
  (c: b a)
  : Type
  := Type
/-   ^~~^ 尝试将这里的“Type”换成“_” -/

def myTheorem := myLemma Type (id Type) Type

当你把 := Type 替换成 := _ 时,你在代码中就留下了一个“洞(hole,又叫 goal)”,证明器会告诉你在 _ 的位置要求填什么类型的值,并且上下文中都有哪些变量可以使用。证明器会输出类似这样的信息:

.github/README.md:?:?: unsolved placeholder:
  ?u.? : Type

context:
  (a: Type)
  (b: (_: Type) → Type)
  (c: (b a))

而所谓的自动定理证明(ATP,automatic theorem proving),则是根据上下文可用的变量,自动填入符合类型限制的值。

TinyLean 能实现部分 ATP 的功能,可以填补一些“显而易见”的洞。而如你所见,完整的 ATP 是一个十分适合 AI 接手的问题。

隐式参数

TinyLean 支持隐式参数(implicit argument)的特性,将我们的 id 函数改写,可以省去我们对 T 参数的传递,类型检查器能推导出来。

def id1 {T: Type} (a: T): T := a

example := id1 Hello

而实际上,隐式参数的原理就是由证明器帮忙插入 _,来看是否能由证明器根据上下文自动填补答案。以上的例子等同于在 id 的调用中留下 _

example := id _ Hello

此外,如果你想显式地赋予 id1T 的参数,不想由证明器填补,则使用以下语法:

example := id1 (T := Type) Hello

邱奇数

仅用 DTLC,我们仍旧能够表达自然数(natural number),比如运用邱奇数(Church numerals)的方式。

定义 CN 类型:

def CN: Type :=
    (T: Type) -> (S: (n: T) -> T) -> (Z: T) -> T

定义一个数字 3,它形如“零的后继的后继的后继”:

def _3: CN := fun T S Z => S (S (S Z))

定义加法和乘法:

def addCN (a: CN) (b: CN): CN :=
    fun T S Z => (a T S) (b T S Z)

def mulCN (a: CN) (b: CN): CN :=
    fun T S Z => (a T) (b T S) Z

做些简单演算:

def _6: CN := addCN _3 _3
def _9: CN := mulCN _3 _3

相等

编写证明最重要的工具是相等(equality),仅有 1+1 而不能证明 1+1=2 是荒唐的。而仅使用 DTLC,我们依旧可以表达出等式,比如运用Leibniz 等式的方式。

定义 LEq 类型、lRefl(reflexivity,自反性)和 lSym(symmetry,对称性):

def LEq {T: Type} (a: T) (b: T): Type :=
    (p: (v: T) -> Type) -> (pa: p a) -> p b

def lRefl {T: Type} (a: T): LEq a a :=
    fun p pa => pa

def lSym {T: Type} (a: T) (b: T) (p: LEq a b): LEq b a :=
    (p (fun b => LEq b a)) (lRefl a)

让我们证明刚刚的 _9 = _3 + _6

example: LEq _9 (addCN _3 _6) := lRefl _9

归纳数据类型

我们可以用归纳数据类型(inductive data type)来定义一个新的类型,比如我们终于可以有一个更直观的自然数了:

inductive N where
| Z
| S (n: N)
open N

这个定义已经非常接近Peano 公理所定义的自然数:

  1. 0(Z)是一个自然数(N
  2. 对于所有自然数 nn 的后继(S n)也是一个自然数

其加法定义,运用递归(recursion)也更加自然:

def addN (n: N) (m: N): N :=
  match n with
  | Z => m
  | S pred => S (addN pred m)

example := addN (S Z) (S Z)

假设一个归纳数据类型没有任何构造器(constructor),则它就是一个空类型(bottom type,即 ⊥):

inductive Bot where
open Bot

爆炸原理(ex falso)是指我们可以从矛盾中获取出任何事物,我们可以用 nomatch 写出这样的定理:

def exFalso (T: Type) (x: Bot): T := nomatch x

这里,我们凭空拿出来了一个 T 类型的值。

索引类型

归纳数据类型是可以携带参数(parameter)的,携带参数时我们称这样的类型为索引类型(indexed type),因为它“被某个值索引(indexed by a value)”。这样的类型我们还可以称作“归纳集(inductive family)”。

比如在 C++ 中,我们可以用“非类型模板参数(non-type template parameter)”实现 std::array<int, 3> 的写法,此时 3 记录着数组的长度,它只是一个普通的数值。

同样的,我们可以定义一个能在类型上记录长度的 vector 类型:

inductive Vec (A: Type) (n: N) where
| Nil (n := Z)
| Cons {m: N} (a: A) (v: Vec A m) (n := S m)
open Vec

这里的 (n := Z) 意思是指,当我使用 Nil 构造一个空 vector 时,它的类型参数 n 会被填为 Z,代表其长度为 0。

几个长度不同的 vector 的例子:

def v0: Vec Type Z := Nil
def v1: Vec Type (S Z) := Cons N v0
def v2: Vec Type (S (S Z)) := Cons CN v1

依赖模式匹配

索引类型能帮助我们排除掉不可能出现的模式(pattern)。举个例子,当我们使用 Nil 构造一个空 vector,并尝试对它进行 match 匹配时,很明显我们不需要再去考虑 Cons 的情况。这样的特性称作“依赖模式匹配(dependent pattern matching)”。

example :=
  match v0 with
  | Nil => Z

假设我们补充上 Cons 的情况,证明器会报出如下错误:

.github/README.md:?:?: type mismatch:
want:
  (Vec Type N.Z)

got:
  (Vec ?m.? (N.S ?m.?))

所以一个空类型不一定是没有构造器的类型,也有可能是完全没办法构造出来的类型,例如:

inductive Weird (n: N) where
| MkWeird (n := Z)
open Weird

example (A: Type) (x: Weird (S Z)): A := nomatch x

此时 Weird (S Z) 也是一个空类型,因为我们完全没办法构造一个这样类型的值。

新的相等类型

通过索引类型的特性,我们可以定义出更好理解的相等类型了:

inductive Eq {T: Type} (a: T) (b: T) where
| Refl (a := b)
open Eq

addNEq 测试一下我们的 1+1=2

example: Eq (addN (S Z) (S Z)) (S (S Z)) := Refl (T := N)

在目前我们介绍的类型系统世界中,所有类型都同属于 Type 之下,我们没有办法对类型进行二次“归类”,这个 Type 忽然就变成了“新的 any”。这样的坏处在于,我希望 int 类型的默认值是 0,希望 string 类型的默认值是 "",而我能通过一个函数 default::<T> 就能生成这个类型的默认值,这要怎么做到呢?

类型类(typeclass,又叫 trait)则能很好地解决这个问题:

class Default (T: Type) where
  default: T
open Default

有了 Default 这个类(class)后,我们就可以为不同的类型定义 Default 的实例(instance)。

实例

N 类型定义它的默认值 Z

instance: Default N
where
  default := Z

Caution

注意这里 where 关键词需要写到新的一行,因为 Lean 4 语法的灵活性很大,为了保持 TinyLean 语法声明文件的简洁,很多语法歧义尚未处理。

我们写个 (default N) = Z 的证明:

example: Eq Z (default N) := Refl (T := N)

类参数

我们可以使用类参数(class parameter)来检查某个类型(type)是否符合类(class)的要求,例如:

def mustBeDefault (T: Type) [p: Default T] := Type

调用 mustBeDefault 时,我们要求参数 T 符合 Default 这一个类的限制。

example := mustBeDefault N

很明显,N 类型符合这个限制。而当我们传入其他的类型,例如 Bot 时,证明器会告诉我们找不到对应的实例声明:

.github/README.md:?:?: no such instance for class '(Default Bot)'

操作符重载

有了类,操作符重载(operator overloading)也能够轻松实现。在 TinyLean 中,中缀操作符 +-*/ 会被简单地翻译成 addsubmuldiv 的函数调用,所以我们要先定义好对应的类和类方法(class method):

class Add {T: Type} where
  add: (a: T) -> (b: T) -> T
open Add

Note

注意到这个 add 的操作是同构(homogenous)的,也就是输入和输出的类型都一致,更好的定义则是异构(heterogeneous)的,即类似 T → U → V 的定义,在此我们省略异构加法的讨论。

N 类型定义相应的实例:

instance: Add (T := N)
where
  add := addN

这样,我们就能在 1+1=2 的证明中使用中缀操作符了:

example
  : Eq (S (S Z)) ((S Z) + (S Z))
  := Refl (T := N)

🔍 探索

接下来,你可以继续探索以下的世界:

源码

tests/onboard.py 文件开始阅读项目源码。

未知

如果你觉得在“指南”阶段仍有许多困惑,甚至完全没法理解发生了什么,这是正常的。“指南”实际上更像是对 TinyLean 特性的展示,而不是一个正儿八经的定理证明教程,因为这样的优质教程其实是很多的,例如:

这些教程/书籍对我而言,并不是第一次读了就全部懂了,而是三至四年内反复地、片段式地不断重复阅读其中的某些片段才明白的。

而我得坦白,让我真正理解类型论的方式,是自己亲手实现一个又一个类型论。

跃迁

TODO

🫡 致谢

TODO


MIT License Copyright © Anqur

About

Tiny theorem prover with syntax like Lean 4 in <1K LOC

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages