Lean 语言参考手册

11.2. 类型间强制转换🔗

当 Lean 精译器成功构造出一个项并推断出其类型,而所在上下文却期望另一种类型的项时,就会插入类型间强制转换。 在报告错误之前,精译器会尝试合成 CoeT 的实例,从而插入从推断类型到预期类型的强制转换。 这一尝试可能通过两种方式成功:

  1. 可以存在一条经过若干中间类型、从推断类型到预期类型的强制转换链。 这些成链的强制转换根据推断类型和预期类型来选择,而不考虑被强制转换的项。

  2. 可以存在一个从推断类型到预期类型的依赖强制转换。 依赖强制转换除推断类型和预期类型外,还会考虑被强制转换的项,但它们不能成链。

定义非依赖强制转换最简单的方式是实现一个 Coe 实例,这足以合成一个 CoeT 实例。 此实例会参与成链,并且可以应用任意多次。 合成 Coe 实例由表达式的预期类型而非推断类型驱动。 对于至多只能使用一次的实例,或应由推断类型驱动合成的实例,可能需要使用其他强制转换类之一。

定义强制转换

类型 Even 表示偶自然数。

structure Even where number : Nat isEven : number % 2 = 0

强制转换使偶数可用于期望自然数的位置。 coe 属性将该投影标记为强制转换,使其能在证明状态和错误消息中相应显示,具体见实现强制转换一节

attribute [coe] Even.number instance : Coe Even Nat where coe := Even.number

有了这个强制转换,就可以在期望自然数的位置使用偶数。

def four : Even := 4, 4 % 2 = 0 All goals completed! 🐙 5#eval (four : Nat) + 1
5

由于强制转换可以成链,将 Coe Even Nat 实例与已有的从 NatInt 的强制转换连接起来,还会形成一个从 EvenInt 的强制转换:

-1#eval (four : Int) - 5
-1

依赖强制转换用于必须根据被强制转换的具体项来确定能否或如何转换该项的情况:例如,只有可判定命题才能强制转换为 Bool,所以相关命题必须出现在实例类型中,以便该类型能够要求 Decidable 实例。 只要推断类型的所有值都能强制转换为目标类型,就使用非依赖强制转换。

定义依赖强制转换

通过以下实例声明,可将字符串 "four" 强制转换为自然数 4

instance : CoeDep String "four" Nat where coe := 4 4#eval ("four" : Nat)
4

其他字符串会产生普通的类型错误:

#eval Type mismatch "three" has type String but is expected to have type Nat("three" : Nat)
Type mismatch
  "three"
has type
  String
but is expected to have type
  Nat

非依赖强制转换可以成链:如果存在从 αβ 的强制转换以及从 βγ 的强制转换,那么也存在从 αγ 的强制转换。 强制转换链应具有 CoeHead?CoeOut*Coe*CoeTail? 的形式,也就是说,它可以由以下部分组成:

  • 一个可选的 CoeHead α α' 实例,之后是

  • 零个或多个 CoeOut α' 、…、CoeOut α'' 实例,之后是

  • 零个或多个 Coe α'' 、…、Coe β' 实例,之后是

  • 一个可选的 CoeTail β' γ 实例

大多数强制转换都可以实现为 Coe 的实例。 某些特殊情况下则需要 CoeHeadCoeOutCoeTail

CoeHeadCoeOut 实例从推断类型朝预期类型方向成链。 换言之,会使用为该项得到的类型中的信息来解析实例链。 CoeCoeTail 实例从预期类型朝推断类型方向成链,因此会使用预期类型中的信息来解析实例链。 如果这些链在中间相遇,就找到了一个强制转换。 这体现在它们的类型签名中:CoeHeadCoeOut半输出参数用于强制转换的目标,而 CoeCoeTail半输出参数用于强制转换的源。

当实例为半输出参数提供值时,该值会在实例合成期间使用。 但是,如果没有提供值,则合成算法可以为其赋值。 因此,选择实例时,应为每个半输出参数指派一个类型。 这意味着,当强制转换输出中出现的变量是输入中变量的子集时,应使用 CoeOut;当输入中的变量是输出中变量的子集时,则应使用 Coe

CoeOutCoe 实例

Truthy 值由一个值和一个指示该值应视为真还是假的标志配对组成。 Decision 可以是 yesnomaybe,其中最后一种还包含需要考虑的其他数据。

structure Truthy (α : Type) where val : α isTrue : Bool inductive Decision (α : Type) where | yes | maybe (val : α) | no

“Truthy” 值可以通过忽略其中包含的值转换为 BoolBool 可以通过排除 maybe 情况转换为 Decision

@[coe] def Truthy.toBool : Truthy α Bool := Truthy.isTrue @[coe] def Decision.ofBool : Bool Decision α | true => .yes | false => .no

Truthy.toBool 必须是 CoeOut 实例,因为强制转换的目标比源包含更少的未知类型变量;而 Decision.ofBool 必须是 Coe 实例,因为强制转换的源比目标包含更少的变量:

instance : CoeOut (Truthy α) Bool := Truthy.isTrue instance : Coe Bool (Decision α) := Decision.ofBool

有了这些实例,强制转换就可以成链:

Decision.yes#eval ({ val := 1, isTrue := true : Truthy Nat } : Decision String)
Decision.yes

尝试使用错误的类会导致错误:

instance does not provide concrete values for (semi-)out-params Coe (Truthy ) Boolinstance : Coe (Truthy α) Bool := Truthy.isTrue
instance does not provide concrete values for (semi-)out-params
  Coe (Truthy ) Bool
🔗类型类
CoeHead.{u, v} (α : Sort u) (β : semiOutParam (Sort v)) : Sort (max (max 1 u) v)
CoeHead.{u, v} (α : Sort u) (β : semiOutParam (Sort v)) : Sort (max (max 1 u) v)

CoeHead α β 用于在强制转换链开头至多应用一次、按从左到右方向进行的强制转换。

CoeHead.mk.{u, v}
coe : α  β

将类型为 α 的值强制转换为类型 β。可通过记法 x 或双重类型标注 ((x : α) : β) 使用。

🔗类型类
CoeOut.{u, v} (α : Sort u) (β : semiOutParam (Sort v)) : Sort (max (max 1 u) v)
CoeOut.{u, v} (α : Sort u) (β : semiOutParam (Sort v)) : Sort (max (max 1 u) v)

CoeOut α β 用于按从左到右方向应用的强制转换。

CoeOut.mk.{u, v}
coe : α  β

将类型为 α 的值强制转换为类型 β。可通过记法 x 或双重类型标注 ((x : α) : β) 使用。

🔗类型类
CoeTail.{u, v} (α : semiOutParam (Sort u)) (β : Sort v) : Sort (max (max 1 u) v)
CoeTail.{u, v} (α : semiOutParam (Sort u)) (β : Sort v) : Sort (max (max 1 u) v)

CoeTail α β 用于只能出现在强制转换序列末尾的转换。也就是说,α 还可以通过 Coe σ αCoeHead τ σ 实例进一步转换,但 β 只能是表达式的预期类型。

CoeTail.mk.{u, v}
coe : α  β

将类型为 α 的值强制转换为类型 β。可通过记法 x 或双重类型标注 ((x : α) : β) 使用。

存在适当的实例链或单个适用的 CoeDep 实例时,可以合成 CoeT 的实例。Nat 强制转换到另一类型时,NatCast 实例也足够。 如果二者都存在,则优先使用 CoeDep 实例。

🔗类型类
CoeT.{u, v} (α : Sort u) : α (β : Sort v) Sort (max 1 v)
CoeT.{u, v} (α : Sort u) : α (β : Sort v) Sort (max 1 v)

CoeT 是 Lean 在解决类型错误时调用的核心类型类。也可以用记法 x 或双重类型标注 ((x : α) : β) 显式触发它。

CoeT 转换链的文法为 CoeHead? CoeOut* Coe* CoeTail? | CoeDep

CoeT.mk.{u, v}
coe : β

类型为 β 的结果值。输入 x : α 是该类型类的参数,因此这个 β 类型的值可以依赖于 x 上的其他类型类。

依赖强制转换不能成链。 作为强制转换链的替代方案,可以使用 CoeDep α e β 实例将类型为 α 的项 e 强制转换为 β。 依赖强制转换适用于只有部分值可以强制转换的情况;这一机制用于仅将可判定命题强制转换为 Bool。 当值本身出现在强制转换的目标类型中时,它们也很有用。

🔗类型类
CoeDep.{u, v} (α : Sort u) : α (β : Sort v) Sort (max 1 v)
CoeDep.{u, v} (α : Sort u) : α (β : Sort v) Sort (max 1 v)

CoeDep α (x : α) β 是依赖强制转换的类型类:类型 β 可以依赖于 x。更准确地说, 类型类搜索可以使用 x 的值,因而允许实例将 βx 关联起来。

依赖强制转换不参与普通强制转换的传递式链合成;它们必须与类型不匹配的两端精确一致。

CoeDep.mk.{u, v}
coe : β

类型为 β 的结果值。输入 x : α 是该类型类的参数,因此这个 β 类型的值可以依赖于 x 上的其他类型类。

依赖强制转换

非空列表类型可以定义为一个列表与其非空证明组成的二元组。 通过应用投影,可以将此类型强制转换为普通列表:

structure NonEmptyList (α : Type u) : Type u where contents : List α non_empty : contents [] instance : Coe (NonEmptyList α) (List α) where coe xs := xs.contents

该强制转换如预期般工作:

def oneTwoThree : NonEmptyList Nat := [1, 2, 3], [1, 2, 3] [] All goals completed! 🐙 [1, 2, 3, 4]#eval (oneTwoThree : List Nat) ++ [4]

然而,任意列表不能强制转换为非空列表,因为任意选取的某些列表确实可能为空:

instance : Coe (List α) (NonEmptyList α) where coe xs := xs, don't know how to synthesize placeholder for argument `non_empty` context: α:Type u_1xs:List αxs []_
don't know how to synthesize placeholder for argument `non_empty`
context:
α:Type u_1xs:List αxs  []

依赖强制转换可以把强制转换的定义域限制为非空列表:

instance : CoeDep (List α) (x :: xs) (NonEmptyList α) where coe := x :: xs, α:Type ?u.7x:αxs:List αx :: xs [] All goals completed! 🐙 { contents := [1, 2, 3], non_empty := _ }#eval ([1, 2, 3] : NonEmptyList Nat)
{ contents := [1, 2, 3], non_empty := _ }

插入依赖强制转换要求被转换的项在语法上与实例头中的项匹配。 已知非空、但在语法上不是 (· :: ·) 实例的列表,无法使用此实例进行强制转换。

fun xs => let ys := xs ++ [4]; sorry : (xs : List Nat) ?m.14 xs#check fun (xs : List Nat) => let ys : List Nat := xs ++ [4] Type mismatch ys has type List Nat but is expected to have type NonEmptyList Nat(ys : NonEmptyList Nat)

强制转换插入失败时,会报告原始类型错误:

Type mismatch
  ys
has type
  List Nat
but is expected to have type
  NonEmptyList Nat
语法强制转换
term ::= ...
    | term

可以使用前缀运算符 coeNotation : term 显式放置强制转换。

与使用嵌套的类型标注不同,用于放置强制转换的 coeNotation : term 语法不要求显式写出所涉及的类型。

控制强制转换插入

实例合成与强制转换插入会相互作用。 合成实例可能会使类型信息变为已知,随后触发强制转换插入。 强制转换的具体放置位置可能会影响结果。

sub 的这个定义中,会根据函数的返回类型合成 Sub Int 实例。 此实例要求两个参数也为 Int,但它们是 Nat。 减法运算符的每个实参外都会插入强制转换。 这可以从 Lean.Parser.Command.print : command#print 的输出中看出。

def sub (n k : Nat) : Int := n - k def sub : Nat Nat Int := fun n k => n - k#print sub
def sub : Nat  Nat  Int :=
fun n k => n - k

将强制转换运算符放在减法外部,会使精译器先尝试推断减法的类型,再插入强制转换。 因为实参都是 Nat,所以会选择 Sub Nat 实例,从而使差值成为 Nat。 然后再将该差值强制转换为 Int

def sub' (n k : Nat) : Int := (n - k) def sub' : Nat Nat Int := fun n k => (n - k)#print sub'

这两个函数并不等价,因为自然数减法会在零处截断:

-4#eval sub 4 8
-4
0#eval sub' 4 8
0

11.2.1. 实现强制转换🔗

适当的 CoeHeadCoeOutCoeCoeTail 实例足以使所需的强制转换得以插入。 不过,强制转换的实现应使用 coe 属性注册为强制转换。 这会使 Lean 使用 coeNotation : term 运算符显示强制转换的使用。 这也会使 norm_cast 策略将该强制转换视为数值转换,而不是普通函数。

属性强制转换声明
attr ::= ...
    | coe

在函数上标记 @[coe] 属性(该函数通常也应出现在形如 instance : Coe A B := myFn 的声明中),可以让反精译器在打印表达式时把该函数的应用 显示为

实现强制转换

枚举归纳类型 Weekday 表示一周中的各天:

inductive Weekday where | mo | tu | we | th | fr | sa | su

作为一个七元素类型,它与 Fin 7 包含相同的信息。 二者之间存在双射:

def Weekday.toFin : Weekday Fin 7 | mo => 0 | tu => 1 | we => 2 | th => 3 | fr => 4 | sa => 5 | su => 6 def Weekday.fromFin : Fin 7 Weekday | 0 => mo | 1 => tu | 2 => we | 3 => th | 4 => fr | 5 => sa | 6 => su

每种类型都可以强制转换为另一种:

instance : Coe Weekday (Fin 7) where coe := Weekday.toFin instance : Coe (Fin 7) Weekday where coe := Weekday.fromFin

虽然这样可以工作,但 Lean 输出中出现的强制转换实例并未按 Lean 用户所期望的方式使用强制转换运算符呈现。 相反,其中显式使用了名称 Weekday.fromFin

def wednesday : Weekday := (2 : Fin 7) def wednesday : Weekday := Weekday.fromFin 2#print wednesday
def wednesday : Weekday :=
Weekday.fromFin 2

为强制转换的定义添加 coe 属性,会使其使用强制转换运算符显示:

attribute [coe] Weekday.fromFin attribute [coe] Weekday.toFin def friday : Weekday := (5 : Fin 7) def friday : Weekday := 5#print friday
def friday : Weekday :=
5

11.2.2. 自然数与整数的强制转换🔗

类型类 NatCastIntCastCoe 的特殊情况,用于定义从 NatInt 到某种在一定意义上具有典范性的其他类型的强制转换。 它们的存在是为了更好地集成大型数学库,例如 Mathlib;这类库大量使用强制转换,将自然数或整数映射到其他结构(通常是环)。 理想情况下,将自然数或整数强制转换到这些结构所得的形式应为simp 规范形,因为这是一种方便的表示方式。

当强制转换的应用预期成为某类型的simp 规范形时,重要的是实践中所有这类强制转换都应定义相等。 否则,simp 规范形就必须选择唯一一条成链的强制转换路径,但引理却可能不慎使用另一条路径来陈述。 由于 simp 的内部索引基于项的底层结构,而不是项在表层语法中的呈现方式,这些差异会使引理无法在预期位置应用。 另一方面,NatCastIntCast 实例应定义成始终定义相等,从而避免这个问题。 Lean 标准库对实例的安排使得插入强制转换时,会优先选择 NatCastIntCast 实例,而不是强制转换实例链。 它们也可以用作 CoeOut 实例,从而在需要时平稳回退到强制转换链。

🔗类型类
NatCast.{u} (R : Type u) : Type u
NatCast.{u} (R : Type u) : Type u

典范同态 Nat R。在大多数用法中,目标类型具有(半)环结构,而该同态应为(半)环同态。

NatCastIntCast 使不同程序库中可用自然数记法表示的自定义类型能够采用一致的 simp 标准形,而不必建立了解所有组合的强制转换化简集。程序库应尽可能便于通过 NatCast 工作。 例如在 Mathlib 中,只要 R 是带 1 的加法幺半群,就会有这样的同态,因而也会有 NatCast R 实例。

典型示例是 Int.ofNat

NatCast.mk.{u}
natCast : Nat  R

典范映射 Nat R

🔗定义
Nat.cast.{u} {R : Type u} [NatCast R] : Nat R
Nat.cast.{u} {R : Type u} [NatCast R] : Nat R

典范同态 Nat R。在大多数用法中,目标类型具有(半)环结构,而该同态应为(半)环同态。

NatCastIntCast 使不同程序库中可用自然数记法表示的自定义类型能够采用一致的 simp 标准形,而不必建立了解所有组合的强制转换化简集。程序库应尽可能便于通过 NatCast 工作。 例如在 Mathlib 中,只要 R 是带 1 的加法幺半群,就会有这样的同态,因而也会有 NatCast R 实例。

典型示例是 Int.ofNat

🔗类型类
IntCast.{u} (R : Type u) : Type u
IntCast.{u} (R : Type u) : Type u

典范同态 Int R。在大多数用法中,目标类型具有环结构,而该同态应为环同态。

IntCastNatCast 使不同程序库中可用自然数记法表示的自定义类型能够采用一致的 simp 标准形,而不必建立了解所有组合的强制转换化简集。程序库应尽可能便于通过 IntCast 工作。 例如在 Mathlib 中,只要 R 是带 1 的加法群,就会有这样的同态,因而也会有 IntCast R 实例。

IntCast.mk.{u}
intCast : Int  R

典范映射 Int R

🔗定义
Int.cast.{u} {R : Type u} [IntCast R] : Int R
Int.cast.{u} {R : Type u} [IntCast R] : Int R

典范同态 Int R。在大多数用法中,目标类型具有环结构,而该同态应为环同态。

IntCastNatCast 使不同程序库中可用自然数记法表示的自定义类型能够采用一致的 simp 标准形,而不必建立了解所有组合的强制转换化简集。程序库应尽可能便于通过 IntCast 工作。 例如在 Mathlib 中,只要 R 是带 1 的加法群,就会有这样的同态,因而也会有 IntCast R 实例。