原文地址 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
类型系统一个有趣的地方, 有时候我们可以添加新的类型, 即使该类型不包含任何数据, 即使它们唯一的作用是抽象出一些行为, 然后允许你将这些行为绑定到类型上.