16. trait:约束泛型与运算符重载

  • 第十六章,共二十章
  • 阅读约 6 分钟

到目前为止,泛型函数对 T 一无所知——不能比较、不能打印、不能调方法。 trait 给类型参数加上能力约束。声明一个 trait,为具体类型写 impl, 然后用 [T: Trait] 约束泛型:

trait Area[T] {
  fn area(s: T) -> Float
  fn bigger_than(s: T, limit: Float) -> Bool = area(s) > limit
}

type Rect = { w: Float, h: Float }

impl Area[Rect] {
  fn area(s: Rect) -> Float = s.w * s.h
}

fn total_area[T: Area](xs: List[T]) -> Float =
  fold(xs, 0.0, (acc, x) => acc + area(x))

pub fn main() -> Unit !io = {
  let rooms = [Rect { w: 3.0, h: 4.0 }, Rect { w: 2.0, h: 2.0 }]
  println(to_string(total_area(rooms)))
  # trait 方法就是普通函数名,UFCS 点号调用也行
  println(to_string(rooms[0].bigger_than(10.0)))
}
16.0
true

规则很少:trait 恰有一个类型参数;每个「trait × 类型」全程序只允许一个 impl; impl 必须写在 trait 或者主体类型所在的模块里(孤儿规则)。带默认体的方法 (上面的 bigger_than)impl 可以不写,写了就是覆盖。

排序:Ord 与比较运算符

预置 trait Ord[T](唯一方法 cmp(a: T, b: T) -> Int,负/零/正表示小于/等于/大于) 桥接了 < <= > >=:Int/Float/String 天生有序,自定义类型给一个 Ord impl (或直接 derive Ord)就能用比较运算符、当 [T: Ord] 的实参、喂给排序函数:

type Card = { rank: Int, name: String } derive Show, Ord

fn max2[T: Ord](a: T, b: T) -> T = if a < b { b } else { a }

pub fn main() -> Unit !io = {
  let hand = [Card { rank: 3, name: "queen" }, Card { rank: 1, name: "pawn" }]
  # derive Ord 按字段声明顺序逐个比较(和类型先比构造器顺序)
  println(to_string(hand[1] < hand[0]))
  println(max2("pear", "apple"))
  println(to_string(sort([3, 1, 2])))
  println(to_string(map(sort(hand), c => c.name)))
  println(to_string(max_by(hand, c => c.rank)))
}
true
pear
[1, 2, 3]
["pawn", "queen"]
Some(Card { rank: 3, name: "queen" })

配套的列表函数都是稳定排序、平局取第一个:sort/max/min 要求元素有 Ord, sort_by(xs, cmp) 接自定义比较函数,max_by/min_by(xs, key) 按键取极值 (键类型要有 Ord)。

trait 方法,以及任何带约束的函数,都可以当裸函数值传递。它要的是一个期望的函数 类型,因为那才说明约束在哪个类型上解析;剩下的包装连同字典由编译器写出:

fn shout[T: Show](xs: List[T]) -> List[String] = map(xs, to_string)

pub fn main() -> Unit !io = {
  println(join(shout([1, 2, 3]), " "))
  # 这里的约束是 `shout` 自己的,所以包装闭包捕获 `shout` 收到的那个字典,
  # 与手写 `x => to_string(x)` 同形
  println(join(shout(["a", "b"]), " "))
}
1 2 3
"a" "b"

没有期望类型时(比如 let f = to_string),约束没有可解析的类型,编译器会直接 这么说;写出类型,或者手写带标注参数的 lambda。

一个 List 装多种类型:函数字段的 record

List[T] 只装一个 T。想让一个列表装不同类型、而它们都支持同一个操作时,Dawn 没有 dyn Trait 可用。做法是:把那个操作装进一个函数字段的 record,用 opaque 类型把 record 藏起来,再给这个类型写它自己的 impl。约束在打包的地方解析,那是具体类型最后一次还看得见 的位置:

type ShownRepr = { render: fn() -> String }

pub opaque type Shown = ShownRepr

pub fn shown[T: Show](x: T) -> Shown = {
  let r: ShownRepr = ShownRepr { render: () => show(x) }
  r
}

impl Show[Shown] {
  fn show(s: Shown) -> String = {
    let r: ShownRepr = s
    r.render()
  }
}

pub fn main() -> Unit !io = {
  let xs: List[Shown] = [shown(1), shown("two"), shown(true)]
  for x in xs {
    println("${x}")
  }
}
1
"two"
true

第二行带引号。那是 Show[String] 一贯的渲染结果,不是例子写错了。

这一招对「主体只出现在一个位置」的 trait 完整可用:Show、Hash,以及任何 fn(T) -> ... 形状的方法。它够不着 Eq 和 Ord:eq(a: T, b: T) 与 cmp(a: T, b: T) 要两个同一类型的值,而打包扔掉的恰好就是这个事实。所以异构的 List 有,异构的 Map 键没有。这条线为什么落在这里、Dawn 为什么不做 trait 对象, 见 trait.md §10。

impl 的主体可以是泛型的:impl[T: Eq] Eq[List[T]] 是一条条件 impl,泛型类型上的 derive 写出的也是一条(type Box[T] = { v: T } derive Ord 得到 impl[T: Ord] Ord[Box[T]])。v1 的边界:主体的实参必须是这个 impl 自己的、互不相同的 类型参数(impl Eq[List[Int]] 被拒绝);comptime 里不能用 trait 约束的调用。 完整设计见 trait.md。

关联类型,以及 [] 与 for 背后的 trait

trait 可以声明一个属于自己、由每个 impl 填上的类型:trait 里写 type Item,impl 里写 type Item = String。签名经由类型参数够到它,写作 C.Item,于是泛型函数可以谈论 「这个容器装的东西」,而不必知道那是什么:

trait Store[C] {
  type Key
  type Item
  fn fetch(c: C, k: C.Key) -> Option[C.Item]
}

type Shelf = { names: List[String] }

impl Store[Shelf] {
  type Key = Int
  type Item = String
  fn fetch(c: Shelf, k: Int) -> Option[String] = get(c.names, k)
}

# 泛型的使用方:不管 store 的键和元素是什么,照原样用
fn fetch_or[C: Store](c: C, k: C.Key, fallback: C.Item) -> C.Item =
  match fetch(c, k) {
    Some(x) -> x
    None -> fallback
  }

pub fn main() -> Unit !io = {
  let s = Shelf { names: ["tea", "rice"] }
  println(fetch_or(s, 1, "nothing"))
  println(fetch_or(s, 5, "nothing"))
}
rice
nothing

fetch_or 只对着 trait 检查一次;每个调用点上 C.Item 变成该 impl 绑定的类型,所以 fetch_or(s, 1, "nothing") 收一个 String、也返回 String。impl 对每个关联类型恰好 绑定一次;漏绑,或者绑了 trait 没声明的名字,都是错误。

有两个内置 trait 就是这么写的,语言自己的语法正是经过它们。Index[C] 有 type Idx、 type Item 和一个方法 index,c[i] 调的就是它:List(以 Int 为下标)和 Map (以键为下标)自带 impl,给自己的类型写一个 impl 就有了 []。Iter[C] 有 type Cur、 type Item 和四个游标方法,for x in c 走的就是它:

type Grid = { w: Int, cells: List[Int] }

impl Index[Grid] {
  type Idx = (Int, Int)
  type Item = Int
  fn index(g: Grid, p: (Int, Int)) -> Int = {
    let (x, y) = p
    g.cells[y * g.w + x]
  }
}

type Countdown = { from: Int }

impl Iter[Countdown] {
  type Cur = Int
  type Item = Int
  fn iter_start(it: Countdown) -> Int = it.from
  fn iter_done(it: Countdown, c: Int) -> Bool = c == 0
  fn iter_next(it: Countdown, c: Int) -> Int = c - 1
  fn iter_get(it: Countdown, c: Int) -> Int = c
}

pub fn main() -> Unit !io = {
  let g = Grid { w: 3, cells: [1, 2, 3, 4, 5, 6] }
  println("${g[(2, 1)]}")
  let launch = Countdown { from: 3 }
  for n in launch {
    print("${n} ")
  }
  println("liftoff")
}
6
3 2 1 liftoff

两个 trait 都在 prelude 里,所以两个 impl 都不需要 use。[] 只读(没有 c[i] = v), 一个类型也只有一种下标类型。index 本身不能按名字调用,因为 [] 就是它的写法; Iter 的四个方法可以。

另见:spec.md §3.5、§4.8