到目前为止,泛型函数对 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