Make great things

What I cannot create, I do not understand.

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

原文地址 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, 我们这里分别传了 FooBar 类型, 会产生编译错误:

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

posted on 2021-01-28 13:51  wbin91  阅读(286)  评论(0编辑  收藏  举报