Lean 函数式编程

5.1. 结构与继承🔗

为了理解 FunctorApplicativeMonad 的完整定义,还需要另一个 Lean 特性:结构继承。 结构继承允许一种结构类型提供另一种结构的接口,并附带额外字段。 当对具有清晰分类关系的概念建模时,这可能很有用。 例如,考虑一个神话生物模型。 其中有些体型大,有些体型小:

structure MythicalCreature where large : Bool deriving Repr

在幕后,定义 MythicalCreature 结构会创建一个归纳类型,它带有一个名为 mk 的单一构造子:

MythicalCreature.mk (large : Bool) : MythicalCreature#check MythicalCreature.mk
MythicalCreature.mk (large : Bool) : MythicalCreature

类似地,会创建一个函数 MythicalCreature.large,它实际从构造子中提取该字段:

MythicalCreature.large (self : MythicalCreature) : Bool#check MythicalCreature.large
MythicalCreature.large (self : MythicalCreature) : Bool

在大多数古老故事中,每个怪物都能以某种方式被击败。 对一个怪物的描述应当包括这一信息,以及它是否巨大:

structure Monster extends MythicalCreature where vulnerability : String deriving Repr

标题中的 extends MythicalCreature 表明每个怪物也是神话中的存在。 要定义一个 Monster,应同时提供来自 MythicalCreature 的字段和来自 Monster 的字段。 巨魔是一种大型怪物,且易受阳光伤害:

def troll : Monster where large := true vulnerability := "sunlight"

在幕后,继承是用组合实现的。 构造子 Monster.mk 接受一个 MythicalCreature 作为其参数:

Monster.mk (toMythicalCreature : MythicalCreature) (vulnerability : String) : Monster#check Monster.mk
Monster.mk (toMythicalCreature : MythicalCreature) (vulnerability : String) : Monster

除了定义用于提取每个新字段的值的函数之外,还定义了一个类型为 Monster MythicalCreature 的函数 Monster.toMythicalCreature。 这可用于提取底层的生物。

在 Lean 中沿继承层级向上移动,并不等同于面向对象语言中的向上转型。 向上转型运算符会使来自派生类的值被当作父类的一个实例来处理,但该值保留其身份和结构。 然而,在 Lean 中,沿继承层级向上移动实际上会擦除底层信息。 要观察这一点的实际效果,请考虑对 troll.toMythicalCreature 求值的结果:

{ large := true }#eval troll.toMythicalCreature
{ large := true }

只保留 MythicalCreature 的字段。

正如 where 语法一样,带有字段名的花括号记法也适用于结构继承:

def troll : Monster := {large := true, vulnerability := "sunlight"}

然而,委托给底层构造子的匿名尖括号记法会暴露内部细节:

def troll : Monster := Application type mismatch: The argument true has type Bool but is expected to have type MythicalCreature in the application Monster.mk truetrue, "sunlight"
Application type mismatch: The argument
  true
has type
  Bool
but is expected to have type
  MythicalCreature
in the application
  Monster.mk true

需要额外的一组尖括号,这会在 true 上调用 MythicalCreature.mk

def troll : Monster := true, "sunlight"

Lean 的点记法能够将继承纳入考虑。 换言之,已有的 MythicalCreature.large 可以与 Monster 一起使用,并且 Lean 会在调用 MythicalCreature.large 之前自动插入对 Monster.toMythicalCreature 的调用。 然而,这只在使用点记法时发生;若使用普通函数调用语法来应用字段查找函数,则会导致类型错误:

#eval MythicalCreature.large Application type mismatch: The argument troll has type Monster but is expected to have type MythicalCreature in the application MythicalCreature.large trolltroll
Application type mismatch: The argument
  troll
has type
  Monster
but is expected to have type
  MythicalCreature
in the application
  MythicalCreature.large troll

点记法也可以在用户定义函数中考虑继承。 小型生物就是非大型的生物:

def MythicalCreature.small (c : MythicalCreature) : Bool := !c.large

troll.small 求值会得到 false,而尝试对 MythicalCreature.small troll 求值会产生:

Application type mismatch: The argument
  troll
has type
  Monster
but is expected to have type
  MythicalCreature
in the application
  MythicalCreature.small troll

5.1.1. 多重继承🔗

助手是一种神话生物,在得到正确报酬时可以提供帮助:

structure Helper extends MythicalCreature where assistance : String payment : String deriving Repr

例如,nisse 是一种小精灵,据说在得到美味的粥时会帮忙料理家务:

def nisse : Helper where large := false assistance := "household tasks" payment := "porridge"

如果被驯化,巨魔会成为出色的帮手。 它们足够强壮,能够在一夜之间犁完整片田地,不过它们需要模型山羊来使其安于自己的生活境遇。 怪物助手是同时也是帮手的怪物:

structure MonstrousAssistant extends Monster, Helper where deriving Repr

此结构类型的一个值必须填充两个父结构中的所有字段:

def domesticatedTroll : MonstrousAssistant where large := true assistance := "heavy labor" payment := "toy goats" vulnerability := "sunlight"

这两个父结构类型都扩展了 MythicalCreature。 如果以朴素的方式实现多重继承,那么这可能导致“菱形问题”:对于给定的 MonstrousAssistant,应当通过哪条路径到达 large 将不明确。 它应当从所包含的 Monster 中取得 large,还是从所包含的 Helper 中取得? 在 Lean 中,答案是采用第一个指定的到祖父结构的路径,而额外父结构的字段会被复制,并不是让新结构直接同时包含两个父结构。

这一点可以通过考察 MonstrousAssistant 的构造子的签名看出:

MonstrousAssistant.mk (toMonster : Monster) (assistance payment : String) : MonstrousAssistant#check MonstrousAssistant.mk
MonstrousAssistant.mk (toMonster : Monster) (assistance payment : String) : MonstrousAssistant

它以一个 Monster 作为参数,并同时接收 HelperMythicalCreature 之上引入的两个字段。 类似地,虽然 MonstrousAssistant.toMonster 只是从构造子中提取 Monster,但 MonstrousAssistant.toHelper 没有可供提取的 Helper#print 命令会揭示它的实现:

@[reducible] def MonstrousAssistant.toHelper : MonstrousAssistant Helper := fun self => { toMythicalCreature := self.toMythicalCreature, assistance := self.assistance, payment := self.payment }#print MonstrousAssistant.toHelper
@[reducible] def MonstrousAssistant.toHelper : MonstrousAssistant  Helper :=
fun self => { toMythicalCreature := self.toMythicalCreature, assistance := self.assistance, payment := self.payment }

此函数从 MonstrousAssistant 的字段构造一个 Helper@[reducible] 属性与写作 abbrev 具有相同效果。

5.1.1.1. 默认声明🔗

当一个结构继承自另一个结构时,可以使用默认字段定义,基于子结构的字段来实例化父结构的字段。 如果需要比判断某个生物是否大型更精细的大小特异性,则可以将一个专门描述大小的数据类型与继承结合使用,从而得到一个结构,其中 large 字段由 size 字段的内容计算而来:

inductive Size where | small | medium | large deriving BEq structure SizedCreature extends MythicalCreature where size : Size large := size == Size.large

然而,这个默认定义只是一个默认定义。 不同于 C# 或 Scala 等语言中的属性继承,子结构中的定义只会在没有为 large 提供具体值时使用,并且可能出现不合情理的结果:

def nonsenseCreature : SizedCreature where large := false size := .large

如果子结构不应偏离父结构,则有几种选择:

  1. 记录这种关系,就像对 BEqHashable 所做的那样

  2. 定义一个命题,说明这些字段以适当方式相关;并设计 API,使其在关键处要求该命题为真的证据

  3. 完全不使用继承

第二种选择可以如下所示:

abbrev SizesMatch (sc : SizedCreature) : Prop := sc.large = (sc.size == Size.large)

注意,单个等号用于表示相等性命题,而双等号用于表示一个检查相等性并返回 Bool 的函数。 SizesMatch 被定义为 abbrev,因为它应当在证明中自动展开,使得 decide 能够看到应当证明的等式。

huldre 是一种中等大小的神话生物——事实上,它们与人类一样高大。 huldre 上的两个大小字段彼此一致:

def huldre : SizedCreature where size := .medium example : SizesMatch huldre := SizesMatch huldre All goals completed! 🐙

5.1.1.2. 类型类继承🔗

在幕后,类型类就是结构。 定义一个新的类型类会定义一个新的结构,而定义一个实例会创建该结构类型的一个值。 随后它们会被加入 Lean 的内部表中,使 Lean 能够在需要时找到这些实例。 由此可知,类型类可以从其他类型类继承。

由于类型类继承使用的正是相同的语言特性,它支持结构继承的所有特性,包括多重继承、父类型方法的默认实现,以及菱形结构的自动折叠。 这在许多与 Java、C# 和 Kotlin 等语言中的多接口继承有用的相同场景中也很有用。 通过仔细设计类型类继承层次,程序员可以同时获得两方面的优点:一组细粒度、可独立实现的抽象,以及从更大、更一般的抽象自动构造这些特定抽象的能力。