Seeking solutions to the `MyType`, `View`, `ViewMut` boilerplate problem


#1

Here’s a pattern that frequently arises in my code. For instance, this thing resembling a matrix:

pub type Basis<T> = RawBasis<Vec<T>>;
pub type Slice<'a, T: 'a> = RawBasis<&'a [T]>;
// not pictured: SliceMut

#[derive(Debug, Copy, Clone)]
pub struct RawBasis<V> {
    dim: usize, // width of each row
    data: V,
}

impl<T> Basis<T> {
    pub fn as_ref(&self) -> Slice<T> {
        let RawBasis { dim, ref data } = *self;
        RawBasis { dim, data: &data[..] }
    }
}

Or how about this struct of arrays:

pub struct Soa(Vec<u8>, Vec<f64>);
pub struct Slice<'a>(&'a [u8], &'a [f64]);

impl Soa {
    pub fn as_ref(&self) -> Slice {
        Slice(&self.0[..], &self.1[..])
    }
}

Basically, what happens is that I have a type like Basis or Soa, and methods implemented on that type; but then I find that I want to use these methods on borrowed data. So I create Slice and SliceMut types, move the methods over accordingly… and then I have to add a bunch a bunch of delegated stubs for the other ownership variants. I want to reduce this boilerplate somehow.

use ::std::slice;

// example of a method I probably want
pub type Iter<'a, T> = slice::Chunks<'a, T>;
impl<'a, T> Slice<'a, T> {
    pub fn iter(&self) -> Iter<'a, T> { self.data.chunks(self.dim) }
}

// well, shucks, now I gotta write wrappers for the other ownership variants.
impl<T> Basis<T> {
    pub fn iter(&self) -> Iter<T> { self.as_ref().iter() }
}

I’ve had some crazy ideas. Like, in the case of Basis, maybe I can write this as a DST. Then Deref would give me all of the good stuff for free!

…it is shortly after this point that I realize I have no idea where to even begin.

pub type Basis<T> = Box<RawBasis<T>>
pub type Slice<'a, T> = &'a RawBasis<T>
pub struct RawBasis<T> {
    dim: usize,
    data: [T],
}

impl Basis<T: Clone> {
    fn filled(fill: T, dim: usize, rank: usize) -> Basis<T> {
        // okay, just gotta make a Box<RawBasis<T>>
        //  whose data resembles vec![fill; dim * rank]

        // uh.......

        // uh.............
    }
}

#2

What’s the purpose of the Slice type? Why not return slices directly via Deref (as you alluded to)?


#3

Slice's API does not resemble a slice, it resembles a Basis. It implements every method of Basis that I originally might have written as taking an &self receiver.

For instance, Slice<f64> will have methods like total_probability(&self, ket: &[f64]) -> f64, and iterating over one will produce &[f64] rows of length dim rather than just &f64 elements.


When I write these types they’re usually highly domain-specific and are the sort of thing that I don’t want to pull in a public external dependency for… which makes all the necessary boilerplate all the more sad.


#4

Ok, I see.

Your RawBasis struct sort of looks like the RcBox used by Rc so maybe you can play the same tricks that it uses to store DSTs along with inline sized values. I’m not sure there’s a way to create a Box<SomeDST> where SomeDST has other fields besides the DST.

I guess you don’t want to use macros to cut down on boilerplate?


#5

This is interesting. I can almost smell the beginnings of a possible proc_macro brewing in my head.

Hm. If only I could just somehow allocate some sort of… uninitialized Box without nightly APIs…


#6

What a coincidence. That seems to almost exactly describe the malloc() function from libc

Regardless, it sounds like you are wanting to abstract over owned/borrowed, and mutable/immutable. I’m not sure how you’d abstract over mutability, but I reckon you could get pretty far by having an owned and borrowed type (like &[T] vs Vec<T>) where the owned one just Deref's to the borrowed type.

Otherwise could something like Cow<T> help here?


#7

Regardless, it sounds like you are wanting to abstract over owned/borrowed, and mutable/immutable. I’m not sure how you’d abstract over mutability, but I reckon you could get pretty far by having an owned and borrowed type (like &[T] vs Vec<T>) where the owned one just Deref's to the borrowed type.

I can almost never implement Deref. (See both examples in my OP.)

Returning an &R is a very tough constraint.


Tbh this is something that kills me about the current state of Rust. We have tools like Deref and Index that work just fine for the core and standard library types and nothing else. Unlike the compiler, mere mortals like us cannot define special semantics for &MyType.


#8

I don’t have a satisfactory solution to this problem, even though I run into this very frequently as I often work with oddly-shaped data structures.

The problem is in part exacerbated by the lack of HKTs, which hopefully the ongoing RFC will help address. In my (unpopular) opinion, dynamically sized types are a hack to work around the lack of HKTs. References like &T when T is unsized have totally different representations from when T is sized, and Rust tries really hard to pretend they are the same thing.

The usability issues of general reference-like types don’t get a lot of attention because & covers most of the use cases that users care about - the language and standard library have an inherent bias to the & type constructor. The few users who need to work with more general reference-like types end up having to reimplement these things in a more clunky way.


#9

I had the same issue with in the soa-derive crate: I need to create one wrapper type for all of reference, mutable reference, slice and mutable slice (see the example, where all 4 wrapper types are created.)

The issue comes from the inability to return &XXXSlice from the Deref implementation, because such reference would point to some stack allocated data. It could be fixed by the new ATC stuff, but this would require changing the definition of Deref and might be backward incompatible.

The solution I picked is code generation: I generate all these types and all the methods that Deref would allow to call inside the proc macro. But this is still tedious and takes a long time to write.