LEAN - 1. 依赖类型理论

依赖类型理论是一种强大且富有表现力的语言,它可以表达复杂的数学断言,编写复杂的硬件和软件规范,并以一种自然和统一的方式进行推理。Lean 基于一种称为构造演算(Calculus of Constructions)的依赖类型理论,具有非累积宇宙(non-cumulative universes)和归纳类型的可数层次结构。

简单类型理论

在类型论中,来源于每个表达式都有一个关联的类型。 例如,在给定的上下文中,x+0 可能表示自然数,而 f 可能表示自然数上的函数。Lean 中的自然数可以说是真正意义上的一个任意精度的无符号整数(即数学定义中的自然数)。

下面是一些例子,介绍怎样在 Lean 中声明一个数学对象并且检查(check)该对象的类型。

/- Define some constants. -/

def m : Nat := 1       -- m is a natural number
def n : Nat := 0
def b1 : Bool := true  -- b1 is a Boolean
def b2 : Bool := false

/- Check their types. -/

#check m            -- output: Nat
#check n
#check n + 0        -- Nat
#check m * (n + 0)  -- Nat
#check b1           -- Bool
#check b1 && b2     -- "&&" is the Boolean and
#check b1 || b2     -- Boolean or
#check true         -- Boolean "true"

/- Evaluate -/

#eval 5 * 4         -- 20
#eval m + 2         -- 3
#eval b1 && b2      -- false

/--/之间的任何文本都构成了 Lean 忽略的注释块。 同样,两个破折号--表示该行的其余部分包含一个也被忽略的注释。 注释块可以嵌套,从而可以注释掉代码块,就像许多其他编程语言。

def关键字将新的常量符号声明到工作环境中。在上面的示例中,def m : Nat := 1定义了一个新的Nat类型常量m,其值为1#check命令要求 Lean 报告它们的类型,在 Lean 中,查询系统信息的辅助命令通常以井号#开头。#eval命令要求 Lean 确定(evaluate)给定的表达式。您可以尝试自己声明一些常量并检查一些表达式。

简单类型理论的强大之处在于您可以从其他类型中构建新类型。 例如,如果ab是类型,a -> b表示从ab的函数类型,a × b表示由a的一个元素与b的一个元素组成的元素对的类型,称为笛卡尔积。请注意,×是一个 Unicode 符号。Unicode 的使用提高了易读性,所有现代编辑器基本都支持它。 在 Lean 标准库中,您经常会看到表示类型的希腊字母,而 Unicode 符号作为->的更紧凑版本。

#check Nat → Nat      -- type the arrow as "\to" or "\r"
#check Nat -> Nat     -- alternative ASCII notation

#check Nat × Nat      -- type the product as "\times"
#check Prod Nat Nat   -- alternative notation

#check Nat → Nat → Nat
#check Nat → (Nat → Nat)  --  same type as above

#check Nat × Nat → Nat
#check (Nat → Nat) → Nat -- a "functional"

#check Nat.succ     -- Nat → Nat
#check (0, 1)       -- Nat × Nat
#check Nat.add      -- Nat → Nat → Nat

#check Nat.succ 2   -- Nat
#check Nat.add 3    -- Nat → Nat
#check Nat.add 5 2  -- Nat
#check (5, 9).1     -- Nat
#check (5, 9).2     -- Nat

#eval Nat.succ 2   -- 3
#eval Nat.add 5 2  -- 7
#eval (5, 9).1     -- 5
#eval (5, 9).2     -- 9

让我们看一些基本的语法。

您可以通过键入\to\r\->来输入 Unicode 箭头。 您也可以使用 ASCII 替代 ->,因此表达式Nat -> NatNat → Nat的含义相同。 这两个表达式都表示将自然数作为输入并返回自然数作为输出的函数类型。 笛卡尔积的 Unicode 符号×输入为\times。您通常会使用小写希腊字母(如 αβγ)来划分类型。 您可以使用\a\b\g输入这些特定的值。

还有一些需要注意的地方。

  1. 将函数f应用于值x表示为f x(例如,Nat.succ 2)。
  2. 写类型表达式时,箭头向右关联。(例如,Nat.add的类型是Nat → Nat → Nat,等价于Nat → (Nat → Nat))。

因此,您可以将Nat.add视为接受自然数返回另一个函数(该函数接受自然数并返回自然数)。 在类型论中,这通常比将Nat.add编写为一个将一对自然数作为输入并返回一个自然数作为输出的函数更方便。 例如,它允许您部分应用函数Nat.add。上面的例子表明Nat.add 3的类型是Nat → Nat,也就是说,Nat.add 3返回一个“等待”第二个参数n的函数,这相当于写Nat.add 3 n

如果有m : Natn : Nat,则(m, n)表示mn的有序对,其类型为Nat × Nat。这提供了一种创建自然数对的方法。相反,如果有p : Nat × Nat,那么你可以写出p.1 : Natp.2 : Nat。这提供了一种提取该类对象的两个部分的方法。

对象类型

Lean 的依赖类型理论扩展成简单类型理论的一种方式是类型本身 —— 像NatBool这样的实体是一等公民,也就是说它们本身就是对象。 为此,它们中的每一个都必须有一个类型。

#check Nat               -- Type
#check Bool              -- Type
#check Nat → Bool        -- Type
#check Nat × Bool        -- Type
#check Nat → Nat         -- ...
#check Nat × Nat → Nat
#check Nat → Nat → Nat
#check Nat → (Nat → Nat)
#check Nat → Nat → Bool
#check (Nat → Nat) → Nat

可以看到上面的每一个表达式都是一个Type类型的对象。 您还可以为类型声明新常量:

def α : Type := Nat
def β : Type := Bool
def F : TypeType := List
def G : TypeTypeType := Prod

#check α        -- Type
#check F α      -- Type
#check F Nat    -- Type
#check G α      -- Type → Type
#check G α β    -- Type
#check G α Nat  -- Type

正如上面的例子所展示的,你已经看到了一个类型为Type → Type → Type的函数的例子,即笛卡尔积Prod

def α : Type := Nat
def β : Type := Bool

#check Prod α β       -- Type
#check α × β          -- Type

#check Prod Nat Nat   -- Type
#check Nat × Nat      -- Type

这是另一个例子:给定任何类型α,类型List α表示类型α的元素列表的类型。

def α : Type := Nat

#check List α    -- Type
#check List Nat  -- Type

鉴于 Lean 中的每个表达式都有一个类型,很自然地要问:Type本身有什么类型?

#check Type      -- Type 1

您实际上遇到了 Lean 类型系统最微妙的方面之一。 精益的底层基础具有无限的类型层次结构:

#check Type     -- Type 1
#check Type 1   -- Type 2
#check Type 2   -- Type 3
#check Type 3   -- Type 4
#check Type 4   -- Type 5

将类型0视为“小”或“普通”类型的宇宙。 类型1是一个更大的类型域,其中包含类型0作为一个元素,类型2是一个更大的类型域,其中包含类型1作为一个元素。 该列表是不确定的,因此每个自然数n都有一个类型nTypeType 0的缩写:

#check Type
#check Type 0

然而,一些操作需要在类型宇宙上是多态的。 例如,List α应该对任何类型α都有意义,无论α存在于哪个类型的 Universe。这解释了函数List的类型注释:

#check List    -- Type u_1 → Type u_1

这里u_1是一个跨越类型级别的变量。#check命令的输出意味着只要α具有类型n,列表α也具有类型n。 函数Prod具有类似的多态性:

#check Prod    -- Type u_1 → Type u_2 → Type (max u_1 u_2)

要定义多态常量,Lean 允许使用Universe命令显式声明Universe变量:

universe u

def F (α : Type u) : Type u := Prod α α

#check F    -- Type u → Type u

您可以通过在定义F时提供Universe参数来避免使用Universe命令。

def F.{u} (α : Type u) : Type u := Prod α α

#check F    -- Type u → Type u

函数的抽象和计算

Lean 提供了fun(或 λ)关键字创建函数表达式,如下所示:

#check fun (x : Nat) => x + 5   -- Nat → Nat
#check λ (x : Nat) => x + 5     -- λ and fun mean the same thing
#check fun x : Nat => x + 5     -- Nat inferred
#check λ x : Nat => x + 5       -- Nat inferred

可以通过传递所需的参数来计算λ函数:

#eval (λ x : Nat => x + 5) 10    -- 15

从一个表达式创建函数是一个称为 lambda 抽象的过程。 假设你有变量x : α并且你可以构造一个表达式t : β,那么表达式fun (x : α) => t或者等价的λ (x : α) => t是一个类型的对象α → β。将此视为从αβ的函数,它将任何值x映射到值t

下面是一些例子:

#check fun x : Nat => fun y : Bool => if not y then x + 1 else x + 2
#check fun (x : Nat) (y : Bool) => if not y then x + 1 else x + 2
#check fun x y => if not y then x + 1 else x + 2   -- Nat → Bool → Nat

Lean 将最后三个示例解释为相同的表达式; 在最后一个表达式中,Lean 从表达式中推断出xy的类型if not y then x + 1 else x + 2

一些数学上常见的函数操作可以用 lambda 抽象来描述:

def f (n : Nat) : String := toString n
def g (s : String) : Bool := s.length > 0

#check fun x : Nat => x        -- Nat → Nat
#check fun x : Nat => true     -- Nat → Bool
#check fun x : Nat => g (f x)  -- Nat → Bool
#check fun x => g (f x)        -- Nat → Bool

思考这些表达式的意思。 表达式fun x : Nat => x表示Nat上的恒等函数,表达式fun x : Nat => true表示始终返回true的常量函数,而fun x : Nat => g (fx)表示fg的复合。通常,您可以省略类型注释并让 Lean 为您推断它。比如,您可以写fun x => g (f x)来代替fun x : Nat => g (f x)

您可以将函数作为参数传递,并通过给它们命名fg然后您可以在实现中使用这些函数:

#check fun (g : StringBool) (f : Nat → String) (x : Nat) => g (f x)
-- (String → Bool) → (Nat → String) → Nat → Bool

您还可以将类型作为参数传递:

#check fun (α β γ : Type) (g : β → γ) (f : α → β) (x : α) => g (f x)

例如,最后一个表达式表示函数,它采用αβγ三种类型和两个函数g : β → γf : α → β,并返回gf的复合。(理解这个函数的类型需要了解???,我们会在下面解释。)

lambda 表达式的一般形式是fun x : α => t,其中变量x是“绑定变量”:它实际上是一个占位符,其“范围”不会超出表达式t。例如,表达式fun (b : β) (x : α) => b中的变量b与前面声明的常量b无关。事实上,该表达式表示的函数与fun (u : β) (z : α) => u相同。

形式上,在重命名绑定变量之前相同的表达式称为alpha 等效表达式,并被认为是“相同的”。Lean 认识到这种等价性。

请注意,将项t : α → β应用于项s : α会产生表达式t s : β。 回到前面的例子,为了清晰地重命名绑定变量,注意以下表达式的类型:

#check (fun x : Nat => x) 1     -- Nat
#check (fun x : Nat => true) 1  -- Bool

def f (n : Nat) : String := toString n
def g (s : String) : Bool := s.length > 0

#check
  (fun (α β γ : Type) (u : β → γ) (v : α → β) (x : α) => u (v x)) Nat String Bool g f 0
  -- Bool

正如所料,表达式(fun x : Nat => x) 1具有Nat类型。 事实上,将表达式(fun x : Nat => x)应用于1应该“返回”值1

#eval (fun x : Nat => x) 1     -- 1
#eval (fun x : Nat => true) 1  -- true

稍后您将看到如何计算这些对象。依赖类型理论的一个重要特征:每个对象都有计算行为,并支持归一化的概念。原则上,减少到相同值的两项称为定义上相等。Lean 的类型检查器认为它们“相同”,并且 Lean 尽最大努力识别和支持这些标识。

Lean 是一门完整的编程语言。 它有一个可以生成二进制可执行文件的编译器和一个交互式解释器。 您可以使用命令#eval来执行表达式,它是测试函数的首选方式。

定义

回想一下,def关键字提供了一种声明新对象的重要方法。

def double (x : Nat) : Nat :=
  x + x

如果您知道函数在其他编程语言中是如何工作的,那么您可能对此更熟悉。名称double被定义为一个函数,它接受一个Nat类型的输入参数x,其中调用的结果是x + x,因此它返回类型Nat。然后,您可以使用以下方法调用此函数:

#eval double 3    -- 6

在这种情况下,您可以将def视为一种命名的lambda。 以下产生相同的结果:

def double : Nat → Nat :=
  fun x => x + x

#eval double 3    -- 6

Lean 有足够的信息来推断它时,你可以省略类型声明。 类型推断是 Lean 的重要组成部分:

def double :=
  fun (x : Nat) => x + x

定义的一般形式是def foo : α := bar,其中α是从表达式bar返回的类型。Lean 通常可以推断出类型α,但明确地编写它是被推荐的行为,因为这阐明了您的意图,如果定义的右侧没有匹配的类型,Lean将标记错误。

右侧栏可以是任何表达式,而不仅仅是 lambda。 所以def也可以用来简单地命名一个值,如下所示:

def pi := 3.141592654

def可以接受多个输入参数。 让我们创建一个将两个自然数相加的结果:

def add (x y : Nat) :=
  x + y

#eval add 3 2               -- 5

参数列表可以这样分开:

def add (x : Nat) (y : Nat) :=
  x + y

#eval add (double 3) (7 + 9)  -- 22

注意这里我们调用了double函数来创建第一个要添加的参数。

你可以在def中使用其他更有趣的表达式:

def greater (x y : Nat) :=
  if x > y then x
  else y

您应该能猜到这个例子的意图。

您还可以定义一个将另一个函数作为输入的函数。 以下调用给定函数两次,将第一次调用的输出传递给第二次:

def doTwice (f : Nat → Nat) (x : Nat) : Nat :=
  f (f x)

#eval doTwice double 2   -- 8

现在为了更抽象一点,您还可以指定类似于类型参数的参数:

def compose (α β γ : Type) (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

这意味着compose是一个将任意两个函数作为输入参数的函数,只要这些函数每个只接受一个输入即可。类型代数β → γα → β意味着要求第二个函数的输出类型必须与第一个函数的输入类型匹配。这是有道理的,否则这两个函数将不可组合。

compose还接受为α类型第三个参数,用于调用第二个函数(命名为f),并将该函数的结果(类型为β)作为输入传递给第一个函数(命名为g)。第一个函数返回一个类型γ,因此这也是compose函数的返回类型。

compose也非常通用,因为它适用于任何类型的α β γ。 这意味着compose可以组合任何两个函数,只要它们每个都带有一个参数,并且第二个的输出类型与第一个的输入相匹配。例如:

def square (x : Nat) : Nat :=
  x * x

#eval compose Nat Nat Nat double square 3  -- 18

局部定义

Lean 还允许您使用let关键字引入“局部”定义。表达式let a := t1; t2定义上等于用t1替换t2中每次出现的a的结果。

#check let y := 2 + 2; y * y   -- Nat
#eval  let y := 2 + 2; y * y   -- 16

def twice_double (x : Nat) : Nat :=
  let y := x + x; y * y

#eval twice_double 2   -- 16

此处,double_double x定义上等于项(x + x) * (x + x)

您可以通过链接let语句来组合多个赋值:

#check let y := 2 + 2; let z := y + y; z * z   -- Nat
#eval  let y := 2 + 2; let z := y + y; z * z   -- 64

;在使用换行符时可以省略。

def t (x : Nat) : Nat :=
  let y := x + x
  y * y

注意表达式let a := t1; t2的含义与(fun a => t2) t1的含义非常相似,但两者并不相同。在第一个表达式中,您应该将t2中的每个a实例视为t1的语法缩写。在第二个表达式中,a是一个变量,表达式fun a => t2必须独立于a的值而有意义。let构造是一种更强的缩写方式,并且存在let a := t1; t2形式的表达式不能表示为(fun a => t2) t1。作为一个练习,试着理解为什么类型检查了下面的foo的定义,而bar的定义却没有。

def foo := let a := Nat; fun x : a => x + 2
/-
  def bar := (fun a => fun x : a => x + 2) Nat
-/

变量和节

考虑以下三个函数的定义:

def compose (α β γ : Type) (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

def doTwice (α : Type) (h : α → α) (x : α) : α :=
  h (h x)

def doThrice (α : Type) (h : α → α) (x : α) : α :=
  h (h (h x))

Lean 为您提供了变量命令以使此类声明看起来更紧凑:

variable (α β γ : Type)

def compose (g : β → γ) (f : α → β) (x : α) : γ :=
  g (f x)

def doTwice (h : α → α) (x : α) : α :=
  h (h x)

def doThrice (h : α → α) (x : α) : α :=
  h (h (h x))

您可以声明任何类型的变量,而不仅仅是Type本身:

variable (α β γ : Type)
variable (g : β → γ) (f : α → β) (h : α → α)
variable (x : α)

def compose := g (f x)
def doTwice := h (h x)
def doThrice := h (h (h x))

#print compose
#print doTwice
#print doThrice

将它们打印出来表明所有三组定义具有完全相同的效果。

variable命令指示 Lean 将声明的变量作为绑定变量插入到按名称引用它们的定义中。Lean 足够聪明,可以确定在定义中显式或隐式使用了哪些变量。 因此,当您编写定义时,您可以将αβγgfhx视为固定对象,并让 Lean 自动为您抽象定义。

以这种方式声明时,变量会一直保留在范围内,直到您正在处理的文件结束。但是,有时限制变量的范围很有用。为此,Lean 提供了section的记号:

section useful
  variable (α β γ : Type)
  variable (g : β → γ) (f : α → β) (h : α → α)
  variable (x : α)

  def compose := g (f x)
  def doTwice := h (h x)
  def doThrice := h (h (h x))
end useful

当该节(section)关闭时,变量超出范围,将会撤出内存。

您不必缩进一个节中的行。 你也不必命名一个节,也就是说,你可以使用一个匿名section/end命令对。 但是,如果您确实命名了一个节,则必须使用相同的名称关闭它。节也可以嵌套,这允许您增加声明新变量。

命名空间

Lean 使您能够将定义分组到嵌套的分层命名空间中:

namespace Foo
  def a : Nat := 5
  def f (x : Nat) : Nat := x + 7

  def fa : Nat := f a
  def ffa : Nat := f (f a)

  #check a
  #check f
  #check fa
  #check ffa
  #check Foo.fa
end Foo

-- #check a  -- error
-- #check f  -- error
#check Foo.a
#check Foo.f
#check Foo.fa
#check Foo.ffa

open Foo

#check a
#check f
#check fa
#check Foo.fa

当您声明一个命名空间Foo时,在该命名空间中,您声明的每个标识符都带有前缀“Foo.”。在命名空间中,您可以通过较短的名称来引用标识符,但是一旦结束命名空间,您就必须使用较长的名称。与节不同,命名空间需要一个名称。根级别的空间只有一个匿名命名空间。

open命令将较短的名称带入当前上下文。通常,当您导入一个模块时,您会想要打开它包含的一个或多个命名空间,以访问短标识符。但有时您会希望使用完全限定名称保护此信息,例如,当它们与您要使用的另一个命名空间中的标识符冲突时。因此,命名空间为您提供了一种在工作环境中管理名称的方法。

例如,Lean 将涉及列表的定义和定理的那部分归到到命名空间List中。

#check List.nil
#check List.cons
#check List.map

命令open List将允许您使用更短的名字:

open List

#check nil
#check cons
#check map

像节一样,命名空间可以嵌套:

namespace Foo
  def a : Nat := 5
  def f (x : Nat) : Nat := x + 7

  def fa : Nat := f a

  namespace Bar
    def ffa : Nat := f (f a)

    #check fa
    #check ffa
  end Bar

  #check fa
  #check Bar.ffa
end Foo

#check Foo.fa
#check Foo.Bar.ffa

open Foo

#check fa
#check Bar.ffa

已关闭的命名空间稍后可以重新打开,即使在另一个文件中:

namespace Foo
  def a : Nat := 5
  def f (x : Nat) : Nat := x + 7

  def fa : Nat := f a
end Foo

#check Foo.a
#check Foo.f

namespace Foo
  def ffa : Nat := f (f a)
end Foo

像节一样,嵌套的命名空间必须按照它们打开的顺序关闭。 命名空间和节有不同的用途:命名空间组织数据,节声明用于插入定义的变量。节也可用于界定命令的范围,例如set_optionopen

然而,在许多方面,namespace ... end块的行为与section ... end块的行为相同。特别是,如果您在命名空间中使用variable命令,则其范围仅限于命名空间。 类似地,如果您在命名空间中使用 open命令,当命名空间关闭时,它的效果就会消失。

依赖类型理论的依赖

依赖类型理论中的的“依赖”,一个简短的解释是类型可以取决于参数。 您已经看到了一个很好的例子:类型 List α 依赖于参数 α,而这种依赖关系导致了List NatList Bool 的区别。 再举一个例子,考虑Vector α n类型,即长度为nα元素的向量类型。这种类型取决于两个参数:向量中元素的类型(α:Type)和向量的长度n:Nat

假设您希望编写一个函数cons,它在列表的头部插入一个新元素。cons应该有什么类型?这样的函数首先应该是多态的:即是说希望NatBool或任意类型αcons函数的行为方式相同。因此,将类型作为cons的第一个参数是有意义的,因此对于任何类型 αcons α 都是 α 类型列表的插入函数。 换句话说,对于每个 αcons α 是一个函数,它接受一个元素 a : α 和一个列表 as : List α,并返回一个新列表,所以你有cons α a as : List α

很明显,cons α应该具有类型α → List α → List α。但是cons应该有什么类型呢?第一个猜测可能是Type → α → list α → list α,但仔细想想,这是没有意义的:这个表达式中的α不指代任何东西,而它应该指代Type类型的参数。换句话说,假设α : Type是函数的第一个参数,接下来的两个元素的类型是αList α。这些类型因第一个参数α而异。

def cons (α : Type) (a : α) (as : List α) : List α :=
  List.cons a as

#check cons Nat        -- Nat → List Nat → List Nat
#check cons Bool       -- Bool → List Bool → List Bool
#check cons            -- (α : Type) → α → List α → List α

这是从属函数类型或从属箭头类型的实例。 给定α : Typeβ : α → Type,将β视为α上的类型族,即每个a : α都有一个类型β a。在这种情况下,类型(a : α) → β a表示函数f的类型,其性质为,对于每个a : α,f aβ a的一个元素。 换句话说,f返回的值的类型取决于它的输入。

请注意,(a : α) → β对于任何表达式β : Type都是有意义的。 当β的值取决于a时(例如,上一段中的表达式β a也是如此),(a : α) → β表示依赖函数类型。当β不依赖于a时,(a : α) → β与类型α → β没有区别。 实际上,在依赖类型理论(以及 Lean)中,当β不依赖于a时,α → β只是(a : α) → β的符号。

回到列表的示例,您可以使用命令#check检查以下列表函数的类型。@符号以及圆括号和花括号之间的区别将立即解释。

#check @List.cons    -- {α : Type u_1} → α → List α → List α
#check @List.nil     -- {α : Type u_1} → List α
#check @List.length  -- {α : Type u_1} → List α → Nat
#check @List.append  -- {α : Type u_1} → List α → List α → List α

正如依赖函数类型 (a : α) → β a 通过允许 β依赖于α来概括函数类型 α → β 的概念一样,依赖笛卡尔积类型(a : α) × β a 概括了笛卡尔积 α × β 以同样的方式。 依赖积也称为 sigma类型,也可以写成 Σ a : α, β a。 您可以使用 ⟨a、b⟩ Sigma.mk a b 创建依赖对。

universe u v

def f (α : Type u) (β : α → Type v) (a : α) (b : β a) : (a : α) × β a :=
  ⟨a, b⟩

def g (α : Type u) (β : α → Type v) (a : α) (b : β a) : Σ a : α, β a :=
  Sigma.mk a b

def h1 (x : Nat) : Nat :=
  (f Type (fun α => α) Nat x).2

#eval h1 5 -- 5

def h2 (x : Nat) : Nat :=
  (g Type (fun α => α) Nat x).2

#eval h2 5 -- 5

上面的函数fg为相同的函数。

隐式参数

假设我们有一个列表的实现:

#check Lst          -- Type u_1 → Type u_1
#check Lst.cons     -- (α : Type u_1) → α → Lst α → Lst α
#check Lst.nil      -- (α : Type u_1) → Lst α
#check Lst.append   -- (α : Type u_1) → Lst α → Lst α → Lst α

然后,您可以按如下方式构建Nat列表。

#check Lst.cons Nat 0 (Lst.nil Nat)

def as : Lst Nat := Lst.nil Nat
def bs : Lst Nat := Lst.cons Nat 5 (Lst.nil Nat)

#check Lst.append Nat as bs

因为构造函数在类型上是多态的,所以我们必须重复插入类型Nat作为参数。但是这个信息是多余的:可以从第二个参数5的类型为Nat的事实推断出Lst.cons Nat 5 (Lst.nil Nat)中的参数 α。可以类似地推断Lst.nil Nat中的参数,不是从该表达式中的任何其他内容,而是从它作为参数发送到函数Lst.cons的事实,该函数期望在该位置有一个Lst α类型的元素。

这是依赖类型理论的一个核心特征:术语携带大量信息,并且通常可以从上下文中推断出其中一些信息。 在Lean中,使用下划线 _ 来指定系统应该自动填写信息。 这被称为“隐式参数”。

然而,键入所有这些下划线仍然很乏味。 当函数采用通常可以从上下文推断的参数时,Lean 允许您指定该参数默认情况下应保留为隐式。 这是通过将参数放在花括号中来完成的,如下所示:

universe u
def Lst (α : Type u) : Type u := List α

def Lst.cons {α : Type u} (a : α) (as : Lst α) : Lst α := List.cons a as
def Lst.nil {α : Type u} : Lst α := List.nil
def Lst.append {α : Type u} (as bs : Lst α) : Lst α := List.append as bs

#check Lst.cons 0 Lst.nil

def as : Lst Nat := Lst.nil
def bs : Lst Nat := Lst.cons 5 Lst.nil

#check Lst.append as bs

所有的改变是变量声明时 α : Type u 周围的大括号。 我们也可以在函数定义中用一种方式:

universe u
def ident {α : Type u} (x : α) := x

#check ident         -- ?m → ?m
#check ident 1       -- Nat
#check ident "hello" -- String
#check @ident        -- {α : Type u_1} → α → α

这使得ident的第一个参数是隐含的。从符号上讲,这隐藏了类型的规范,使它看起来好像ident只是接受任何类型的参数。事实上,函数id在标准库中就是这样定义的。我们在这里选择了一个非传统名称,只是为了避免名称冲突。

当使用variable命令声明变量时,也可以将变量指定为隐式:

universe u

section
  variable {α : Type u}
  variable (x : α)
  def ident := x
end

#check ident
#check ident 4
#check ident "hello"

此处对ident的定义与上面的定义具有相同的效果。

Lean 有非常复杂的机制来实例化隐式参数,我们将看到它们可以用来推断函数类型、谓词甚至证明。 用一个术语来实例化这些“空洞”或“占位符”的过程通常被称为精化。 隐含参数的存在意味着有时可能没有足够的信息来精确地确定表达式的含义。 像idList.nil这样的表达式被称为是多态的,因为它可以在不同的上下文中具有不同的含义。

人们总是可以通过编写(e : T)来指定表达式e的类型T。这指示 Lean 的阐述者在尝试解析隐式参数时使用值T作为e的类型。在下面的第二对示例中,此机制用于指定表达式idList.nil的所需类型:

#check List.nil               -- List ?m
#check id                     -- ?m → ?m

#check (List.nil : List Nat)  -- List Nat
#check (id : Nat → Nat)       -- Nat → Nat

数字在 Lean 中被重载,但是当无法推断数字的类型时,精益默认假定它是自然数。因此,下面前两个#check命令中的表达式以相同的方式阐述,而第三个#check命令将2解释为整数。

#check 2            -- Nat
#check (2 : Nat)    -- Nat
#check (2 : Int)    -- Int

然而,有时我们可能会发现我们已经将函数的参数声明为隐式,但现在想要显式提供参数。 如果foo是这样的函数,则符号@foo表示具有显式所有参数的同一函数。

#check @id        -- {α : Type u_1} → α → α
#check @id Nat    -- Nat → Nat
#check @id Bool   -- Bool → Bool

#check @id Nat 1     -- Nat
#check @id Bool true -- Bool

请注意,现在第一个#check命令给出了标识符id的类型,没有插入任何占位符。 此外,输出表明第一个参数是隐式的。

posted on   Black_x  阅读(789)  评论(0编辑  收藏  举报

编辑推荐:
· AI与.NET技术实操系列:基于图像分类模型对图像进行分类
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· 25岁的心里话
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
< 2025年3月 >
23 24 25 26 27 28 1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30 31 1 2 3 4 5

统计

点击右上角即可分享
微信分享提示