面向程序员的Lean 4教程(4) - 结构体

面向程序员的Lean 4教程(4) - 结构体

结构体也是程序员们的老朋友了,它的作用是定义一个数据类型,并给这个数据类型定义一些字段。

结构体

在Lean中,结构体使用structure关键字定义。

structure Point where
    x : Nat
    y : Nat

Lean 4 会为结构体自动生成默认构造函数,名称是 结构体名.mk。也可以通过定义函数来自定义构造函数。

使用 { 字段名 := 值, … } 的语法可以更方便地创建结构体实例。

我们来看一个例子:

-- 定义五行枚举类型
inductive FiveElement : Type where
  | Wood   -- 木
  | Fire   -- 火
  | Earth  -- 土
  | Metal  -- 金
  | Water  -- 水
  deriving Repr

-- 为 FiveElement 定义 ToString 实例
instance : ToString FiveElement where
  toString
  | FiveElement.Wood  => "木"
  | FiveElement.Fire  => "火"
  | FiveElement.Earth => "土"
  | FiveElement.Metal => "金"
  | FiveElement.Water => "水"

-- 定义阴阳枚举类型
inductive YinYang : Type where
  | Yin  -- 阴
  | Yang -- 阳
  deriving Repr

-- 为 YinYang 定义 ToString 实例
instance : ToString YinYang where
  toString
  | YinYang.Yin  => "阴"
  | YinYang.Yang => "阳"

-- 定义天干结构体
structure HeavenlyStem where
  name : String       -- 天干名称
  element : FiveElement -- 五行属性
  yinYang : YinYang   -- 阴阳属性
  deriving Repr

我们来看两种创建结构体实例的方法:

def jia : HeavenlyStem := { name := "甲", element := FiveElement.Wood, yinYang := YinYang.Yang }
def yi : HeavenlyStem := HeavenlyStem.mk "乙" FiveElement.Wood YinYang.Yin

结构体的继承

结构体可以继承另一个结构体,从而获得父结构体的字段和方法。

比如,我们可以为2D的点添加z坐标,从而获得3D的点。

structure Point where
  x : Float
  y : Float
  deriving Repr

structure Point3D extends Point where
  z : Float
  deriving Repr

def redPoint : Point3D :=
  { x := 1.0, y := 2.0, z := 3.0 }

#eval redPoint.x      -- 输出: 1.0
#eval redPoint.y      -- 输出: 2.0
#eval redPoint.z      -- 输出: 3.0

def point2d : Point := { x := 0.0, y := 0.0 }
def point3d : Point3D := { point2d with z := 4.0 }

#eval point3d  -- 输出: { x := 0.0, y := 0.0, z := 4.0 }

函数是Lean4中的一等公民,自然也可以是结构体的一部分。

structure Point where
  x : Float
  y : Float
  distanceToOrigin : Float := Float.sqrt (x * x + y * y)
  deriving Repr

定义在结构体的外面也不影响使用,比如我们给点增加一个计算曼哈顿距离的函数。

def Point.manhattanDistanceToOrigin (p : Point) : Float :=
  Float.abs p.x + Float.abs p.y

调用方法是一样的:

#eval point2d.distanceToOrigin  -- 输出: 5.0
#eval point2d.manhattanDistanceToOrigin  -- 输出: 7.0

伪装成基本类型的结构体

上一节我们学习了一些伪装成基本类型的归纳类型。还有一些基本类型没有介绍到,其实它们都是伪装成基本类型的结构体。

我们先看看Float类型:

info: .\.\.\.\Test3\Basic.lean:47:0: structure Float : Type
number of parameters: 0
fields:
  Float.val : floatSpec.float
constructor:
  Float.mk (val : floatSpec.float) : Float

Float类型是一个结构体,它有一个字段val,类型是floatSpec.float。

FloatSpec又是个啥呢,还是个结构体:

structure FloatSpec where
  float : Type
  val   : float
  lt    : floatfloatProp
  le    : floatfloatProp
  decLt : DecidableRel lt
  decLe : DecidableRel le

val是float类型,而float是Type类型。这是根据系统不同定义的类型,总之是符合IEEE 754标准的浮点数。

我们再来看字符Char类型:

info: .\.\.\.\Test3\Basic.lean:57:0: structure Char : Type
number of parameters: 0
fields:
  Char.val : UInt32
  Char.valid : self.val.isValidChar
constructor:
  Char.mk (val : UInt32) (valid : val.isValidChar) : Char

Char类型是一个结构体,它有一个字段val,类型是UInt32。isValidChar是一个函数,用于判断val是否是一个有效的字符。

我们再来看UInt32类型:

info: .\.\.\.\Test3\Basic.lean:53:0: structure UInt32 : Type
number of parameters: 0
fields:
  UInt32.toBitVec : BitVec 32
constructor:
  UInt32.mk (toBitVec : BitVec 32) : UInt32

UInt32类型也是一个结构体,它有一个字段toBitVec,类型是BitVec 32。

注意,这是一个类型依赖。类型不是BitVec,而是依赖于后面的值的类型。用人话就说就是,不同长度的BitVec不是同一个类型。

info: .\.\.\.\Test3\Basic.lean:73:0: structure BitVec (w : Nat) : Type
number of parameters: 1
fields:
  BitVec.toFin : Fin (2 ^ w)
constructor:
  BitVec.ofFin {w : Nat} (toFin : Fin (2 ^ w)) : BitVec w

这也是我们明明要讲基本数据类型,但是上一节要讲类型依赖的原因。很多语言中非常基础的类型,在Lean中要么是归纳类型,要么是结构体,甚至有些还是类型依赖的类型。

同理,UInt8, UInt16, UInt64也都是依赖不同长度的BitVec。

info: .\.\.\.\Test3\Basic.lean:49:0: structure UInt8 : Type
number of parameters: 0
fields:
  UInt8.toBitVec : BitVec 8
constructor:
  UInt8.mk (toBitVec : BitVec 8) : UInt8
info: .\.\.\.\Test3\Basic.lean:51:0: structure UInt16 : Type
number of parameters: 0
fields:
  UInt16.toBitVec : BitVec 16
constructor:
  UInt16.mk (toBitVec : BitVec 16) : UInt16
info: .\.\.\.\Test3\Basic.lean:53:0: structure UInt32 : Type
number of parameters: 0
fields:
  UInt32.toBitVec : BitVec 32
constructor:
  UInt32.mk (toBitVec : BitVec 32) : UInt32
info: .\.\.\.\Test3\Basic.lean:55:0: structure UInt64 : Type
number of parameters: 0
fields:
  UInt64.toBitVec : BitVec 64
constructor:
  UInt64.mk (toBitVec : BitVec 64) : UInt64

与List是个归纳类型不同,Array是个结构体。

info: .\.\.\.\Test3\Basic.lean:61:0: structure Array.{u} (α : Type u) : Type u
number of parameters: 1
fields:
  Array.toList : List α
constructor:
  Array.mk.{u} {α : Type u} (toList : List α) : Array α

另外,Lean4中还有向量Vector,它继承自Array,但是依赖于长度,每一种长度的Vector是不同的类型。

info: .\.\.\.\Test3\Basic.lean:77:0: structure Vector.{u} (α : Type u) (n : Nat) : Type u
number of parameters: 2
parents:
  Vector.toArray : Array α
fields:
  Array.toList : List α
  Vector.size_toArray : self.size = n
constructor:
  Vector.mk.{u} {α : Type u} {n : Nat} (toArray : Array α) (size_toArray : toArray.size = n) : Vector α n
resolution order:
  Vector, Array

定长的向量和有限的有理数都是类型依赖的经典例子。

字符串

最后,字符串还需要单独拿出来说一下。

字符串是一个结构体,只有一个字段是字符的列表。

info: .\.\.\.\Test3\Basic.lean:63:0: structure String : Type
number of parameters: 0
fields:
  String.data : List Char
constructor:
  String.mk (data : List Char) : String

但是因为不定长字符编码的问题,字符串的长度远比字符列表的长度要复杂。

首先,如果你是在Windows系统上运行Lean 4,可能会遇到显示中文乱码的问题。这是因为Windows默认使用GBK编码,而Lean 4使用的是UTF-8编码。

我们可以通过修改控制台的代码页为 UTF-8(代码页 65001)来支持 UTF-8 字符串的输出。

chcp 65001

在 Lean 4 中,字符串是 Unicode 字符的序列。字符串字面量使用双引号括起来。我们仍然使用lenght函数来获取字符串的长度。

  let s10 := "数学世界666"
  IO.println s10
  let n10 := s10.length
  IO.println n10

运行结果如下:

数学世界666
7

我们知道,utf-8编码的中文字符占用超过一个字节,字符串长度取7并不是按照字符个数计算的。我们可以使用toUTF8函数来将其转换成字节数组,然后计算这个ByteArray的长度:

  let n11 := s10.toUTF8.size
  IO.println n11

我们可以看到,这个字符串占用了15个字节。

字符串的每个字符都是Unicode字符,所以我们可以使用toList函数将其转换成字符列表,然后使用get函数获取每个字符。

  let slist := s10.toList
  IO.println slist
  let c01 := slist[0]
  IO.println c01
  let c02 := slist[1]
  IO.println c02
  let c03 := slist[2]
  IO.println c03

输出如下:

[数, 学, 世, 界, 6, 6, 6] 
数                     
学                     
世                     

但是,如果你使用get函数获取字符串中的字符,你会发现它需要的是一个String.Pos类型。在起点的时候还好办,Lean4会自动推断出起点是0。

  let startPos : String.Pos := 0
  let c000 := s10.get startPos
  IO.println c000

但是,如果你想要获取下一个字符,就需要使用next函数来获取下一个位置:

  let nextPos : String.Pos := s10.next startPos
  let c001 := s10.get nextPos
  IO.println c001
  let nextPos2 : String.Pos := s10.next nextPos
  let c002 := s10.get nextPos2
  IO.println c002

输出如下:

数
学
世

小结

这一节我们学习了结构体,通过结构体,可以定义变量,也可以定义函数,还可以通过继承来扩展结构体。

同时,通过对于基本类型的分析,我们发现了归纳类型和类型依赖在Lean4中的重要性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Jtag特工

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值