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

到目前为止,泛型函数对 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)))
}
在 Playground 打开
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)))
}
在 Playground 打开
true
pear
[1, 2, 3]
["pawn", "queen"]
Some(Card { rank: 3, name: "queen" })

配套的列表函数都是稳定排序、平局取第一个:sort/max/min 要求元素有 Ordsort_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"]), " "))
}
在 Playground 打开
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}")
  }
}
在 Playground 打开
1
"two"
true

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

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

v1 的边界:impl 的主体只能是非泛型具名类型或 Int/Float/Bool/String (没有条件 impl,List[T] 不能做主体);comptime 里不能用 trait 约束的调用。 完整设计见 trait.md