Cheating Higher Ranks with Traits

I ran into this a little while ago and thought it would be helpful to share a possible solution.

Imagine you have an enum that describes a set of possible branches, for each branch there is a type associated with it that you want to run through a function, for this example– serialize.

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),
    }
}

Life is good. Then you realize that you actually needed to format these types in two different ways, one with to_writer and the other with to_writer_pretty. You could make a write and write_pretty function, but that feels dirty. The only thing that would be different in each implementation is the function from serde_json. Naively, it would look something like this:

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),
    }
}

No problem, you think, you’ll parameterize the formatting function and pass it as a closure.

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),
    }
}

But this is folly, write is declared to be valid for any T, but it’s passed a concrete T in the implementation (either Foo or Bar). Indeed, this will generate an error.

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

This is where higher ranked types could come in. If you could declare F: for<T> Fn(&mut W, &T) -> serde_json::Result<()>. Basically, declaring that T is parameterized over the function F and not over write, then this would be valid. But such things are not allowed in Rust today.

How then to solve this problem?

Solutions

Type Erasure

You can usually ‘erase’ a type parameter with a trait object,

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, &dyn Serialize) -> serde_json::Result<()>,
{
    match var {
        Var::One => f(&mut writer, &Foo),
        Var::Two => f(&mut writer, &Bar),
    }
}

The problem with this is that Serialize is not object safe. If a trait has generic methods like Serialize does, it can’t be turned into a trait object. I encourage you to read the link, it’s enlightening.

There are crates like erased_serde that will allow one to make a Box<dyn Serialize>.

Another Enum

You can make another enum representing the different parameters that could be passed to the writer. I don’t think you really gain a lot from this though and I’m not sure it’s much better than just having the two explicit write/write_pretty variants.

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)),
    }
}

But then the closure has to handle the multiple variants also,

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),
    }
})
// and later:
write(var, writer, |writer, foobar| {
    match foobar {
        Foo(foo) => serde_json::to_writer(writer, foo),
        Bar(bar) => serde_json::to_writer(writer, bar),
    }
})

You want to do the same thing for each type. Is there an easier way? You don’t want to add a dependency, and you’re at the point now where this seems like an awful lot of work to try to abstract this bit of code. Maybe you should just copy-paste, make the modifications and be done with it?

Cheating Rank-2

As usual in Rust– traits to the rescue. It’s possible to get around this by creating a trait with the behaviour we’re looking for

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)
    }
}

Now, you can make write parameterized by this 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),
    }
}

Elsewhere, you can simply call the write function with,

write(var, writer, Ugly)?;

Because <T> is bounded over the format function and not the trait itself, it’s effectively the same thing as a rank-2 type (for<T> Fn(T)). I think this shows something interesting about type systems with traits or typeclasses; it’s sometimes valuable to create new types even if they hold no data, even if their only purpose is to abstract a bit of behaviour and allow you to attach it to a type.

If you don’t require a trait definition to have object safety (Format won’t be object safe), traits offer a convenient way to get around higher ranked types. Stick the additional type parameter in a trait method!