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 要求元素有 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"]), " "))
}
在 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 完整可用:Show、Hash,以及任何
fn(T) -> ... 形状的方法。它够不着 Eq 和 Ord:eq(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。