原文地址 https://leshow.github.io/post/cheat_rank_n/
假设你有一个 enum 类描述一组可能的分支, 有一些函数需要对可能的分支进行处理,
而对每个分支存在一个对应的类型, 比如
enum Var {
One,
Two,
}
#[derive(Serialize)]
struct Foo;
#[derive(Serialize)]
struct Bar;
fn write<W>(var: Var, mut writer: W) -> serde_json::Result<()>
where
W: Write,
{
match var {
Var::One => serde_json::to_writer(&mut writer, &Foo),
Var::Two => serde_json::to_writer(&mut writer, &Bar),
}
}
这里我们有枚举 Var, 在 write 函数中, 分支 One 对应 Foo 类型, 分支 Two 对应 Bar 类型. 一切都很美好. 但是当需求变更时, 你可能需要以不同的方式输出这些类型, 一个是 to_writer, 另一个是 to_writer_pretty. 你可以再加一个 write_pretty 函数, 但是这样的实现不优雅. 它们之间的唯一区别是 serde_json中的函数:
fn write<W>(var: Var, mut writer: W) -> serde_json::Result<()>
where
W: Write,
{
match var {
Var::One => serde_json::to_writer(&mut writer, &Foo),
Var::Two => serde_json::to_writer(&mut writer, &Bar),
}
}
fn write_pretty<W>(var: Var, mut writer: W) -> serde_json::Result<()>
where
W: Write,
{
match var {
Var::One => serde_json::to_writer_pretty(&mut writer, &Foo),
Var::Two => serde_json::to_writer_pretty(&mut writer, &Bar),
}
}
你可能会想到将这两函数抽象出来, 添加一个格式化函数作为参数:
fn write<W, T, F>(var: Var, mut writer: W, f: F) -> serde_json::Result<()>
where
W: Write,
T: Serialize + ?Sized,
F: Fn(&mut W, &T) -> serde_json::Result<()>,
{
match var {
Var::One => f(&mut writer, &Foo),
Var::Two => f(&mut writer, &Bar),
}
}
但是这里会有问题, write 函数声明对任意的 T 有效, 而函数 f 的实现需要传实体类型 T, 我们这里分别传了 Foo 和 Bar 类型, 会产生编译错误:
error[E0308]: mismatched types
--> src/lib.rs:23:36
|
16 | fn write<W, T, F>(var: Var, mut writer: W, f: F) -> serde_json::Result<()>
| - this type parameter
...
23 | Var::One => f(&mut writer, &Foo),
| ^^^^ expected type parameter `T`, found struct `Foo`
|
= note: expected reference `&T`
found reference `&Foo`
= help: type parameters must be constrained to match other types
= note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
error[E0308]: mismatched types
--> src/lib.rs:24:36
|
16 | fn write<W, T, F>(var: Var, mut writer: W, f: F) -> serde_json::Result<()>
| - this type parameter
...
24 | Var::Two => f(&mut writer, &Bar),
| ^^^^ expected type parameter `T`, found struct `Bar`
|
= note: expected reference `&T`
found reference `&Bar`
= help: type parameters must be constrained to match other types
= note: for more information, visit https://doc.rust-lang.org/book/ch10-02-traits.html#traits-as-parameters
这个时候我们就需要高阶类型 (higher ranked types). 如果能将 F 声明成 F: for<T> Fn(&mut W, &T) -> serde_json::Result<()>, 这样就可以将 T 从 write 依赖变成依赖 F. 但是现在 Rust 不允许我们这样做.
解决方案
类型消除
通常我们可以通过 trait object 来消除一个类型参数:
fn write<W, F>(var: Var, mut writer: W, f: F) -> serde_json::Result<()>
where
W: Write,
F: Fn(&mut W, &dyn Serialize) -> serde_json::Result<()>,
{
match var {
Var::One => f(&mut writer, &Foo),
Var::Two => f(&mut writer, &Bar),
}
}
这里的问题是 Serialize 不是 object safe. 如果一个 trait 中有泛型方法, 那么我们就不能将其转成 trait object. Serialize 就是一个这样的 trait.
使用 erased_serde 这样的库, 可以让我们使用 Box<dyn Serialize> 这样的 trait object.
使用另一个 Enum
还可以定义另外一个 enum 来包含各种可能的类型. 不过这种方式并不比使用两个 write/write_pretty 更好.
enum Foobar<'a> {
Foo(&'a Foo),
Bar(&'a Bar)
}
fn write<W, T, F>(var: Var, mut writer: W, f: F) -> serde_json::Result<()>
where
W: Write,
T: Serialize + ?Sized,
F: Fn(&mut W, FooBar) -> serde_json::Result<()>,
{
match var {
Var::One => f(&mut writer, Foo(&Foo)),
Var::Two => f(&mut writer, Bar(&Bar)),
}
}
我们必须要在传递的参数 f 中处理新的 enum.
write(var, writer, |writer, foobar| {
match foobar {
Foo(foo) => serde_json::to_writer_pretty(writer, foo),
Bar(bar) => serde_json::to_writer_pretty(writer, bar),
}
})
// 以及:
write(var, writer, |writer, foobar| {
match foobar {
Foo(foo) => serde_json::to_writer(writer, foo),
Bar(bar) => serde_json::to_writer(writer, bar),
}
})
Rank-2 高阶类型
在 Rust 中, 我们可以使用 trait 来解决. 我们可以添加一个如下的 trait:
trait Format {
fn format<W, T>(&self, writer: W, val: &T) -> serde_json::Result<()>
where
W: Write,
T: Serialize + ?Sized;
}
struct Ugly;
struct Pretty;
impl Format for Ugly {
fn format<W, T>(&self, writer: W, val: &T) -> serde_json::Result<()>
where
W: Write,
T: Serialize + ?Sized,
{
serde_json::to_writer(writer, val)
}
}
impl Format for Pretty {
fn format<W, T>(&self, writer: W, val: &T) -> serde_json::Result<()>
where
W: Write,
T: Serialize + ?Sized,
{
serde_json::to_writer_pretty(writer, val)
}
}
这时, 我可以将 write 抽象成使用这个 trait:
fn write<W, F>(var: Var, mut writer: W, format: F) -> serde_json::Result<()>
where
W: Write,
F: Format,
{
match var {
Var::One => format.format(&mut writer, &Foo),
Var::Two => format.format(&mut writer, &Bar),
}
}
可以很方便的使用它:
write(var, writer, Ugly)?;
因为泛型参数 <T> 不是在 Format 上, 而是在 format 函数上, 我们可以将其看成是二阶类型 for<T> Fn(T). 我认为这是 trait 或者 typeclasses 类型系统一个有趣的地方, 有时候我们可以添加新的类型, 即使该类型不包含任何数据, 即使它们唯一的作用是抽象出一些行为, 然后允许你将这些行为绑定到类型上.