Default type parameters on Rust structs: is it possible to provide a default type containing a lifetime?

62 Views Asked by At

I'm (unfortunately) suspecting that what I'd like to do is impossible, so I'm very open to best-practice workarounds.

In short, I've got a setup like the following:

pub struct OffsetMod<'a> {
    kind: OffsetKind,
    composition: ChemicalComposition<'a>,
}

Most of the time, that struct should indeed be the owner of that ChemicalComposition, but sometimes this data is stored differently, and the struct is peeled apart into something approximating HashMap<ChemicalComposition<'a>, OffsetKind>, then I have a method that iterates over this HashMap and returns a series of reconstituted OffsetMod<'a>s. Well, sorta OffsetMod<'a>s... Obviously the only way I could get those owned ChemicalCompositions back (without some very expensive .clone()ing) is to drain / move the HashMap, but that's not something I want to do.

Downstream, I actually only need a reference to those ChemicalCompositions, so really what I want to return is a sort of borrowed OffsetMod<'a> — something like:

pub struct BorrowedOffsetMod<'a> {
    kind: OffsetKind,
    composition: &'a ChemicalComposition<'a>,
}

The issue, however, with creating that second struct, is that I'd now have to duplicate all of the methods / trait impls I have for OffsetMod for BorrowedOffsetMod!

Elsewhere in my code, when encountering a similar case of needing a struct to come in a form that borrows its contents, and another that doesn't, I wrote something like this:

pub struct Target<S = String> {
    pub group: S,
    pub location: Option<S>,
    pub residue: Option<S>,
}

By default, it owns its data (Strings, in this case), but my impl blocks are written as:

impl<S: Borrow<str>> Target<S> {
    // — snip —
}

In this way, I can typically assume that Target owns its data, but I can also "borrow" Target as Target<&'a str>, and all of the same methods will work fine because of the Borrow<str> bound.

Now here comes my very troubling issue:

If I try the same with my OffsetMod struct, the pain begins:

pub struct OffsetMod<'a, C = ChemicalComposition<'a>> {
    kind: OffsetKind,
    composition: C,
}

Which hits me with the ever-awful

error[E0392]: parameter `'a` is never used
help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`

Now, to open with the "best" solution I've found so far, I'm a bit perplexed at why this is so easy and works fine:

pub type OffsetMod<'a> = OffsetModInner<ChemicalComposition<'a>>;
struct OffsetModInner<C> {
    kind: OffsetKind,
    composition: C,
}

When that feels quite a lot like what I'd expect OffsetMod<'a, C = ChemicalComposition<'a>> to do behind the scenes... The reason this solution isn't quite cutting it for me, is that it now "splits" my type in two: my parameters may be able to use OffsetMod<'a> just fine, but for the borrowed version, I can't do OffsetMod<&ChemicalComposition<'a>>, but I need to use that second "hidden" OffsetModInner<&ChemicalComposition<'a>> or make another alias like BorrowedOffsetMod<'a>. Whilst this is the best I can land on, it still feels a bit messy. EDIT: Slightly more than messy, what I've written is invalid, since the OffsetModInner cannot be private if the type OffsetMod containing it is public! I'd have to leak that OffsetKindInner into public API.

Now, I understand that Rust wants this 'a bound to show up in a field somewhere, so that if you're not using the default type, it still knows how to populate that lifetime. Perhaps we could claim that C always lives as long as 'a, since C always shows up in the Composition field.

pub struct OffsetMod<'a, C: 'a = ChemicalComposition<'a>> {
    kind: OffsetKind,
    composition: C,
}

But no dice there either — same error as before. Though I've tried a multitude of other things, I've not found one I'm pleased with:

  1. PhantomData looks and feels hacky, and I'm not certain if it's semantically correct either? I don't know, perhaps I'm more open to this one than I thought...
  2. Something like Cow<'a, ChemicalComposition<'a>> feels a bit nasty and adds runtime overhead where there really doesn't need to be. I'll only ever need references (no need to copy), it's just that these OffsetMod structs are sometimes the only place the underlying data has to live (e.g. for ChemicalCompositions that aren't stored in that HashMap)!
  3. I tried some unholy GAT stuff with a Borrowesque trait that looked like this:
pub trait NestedBorrow<T> {
    type Content<'a>
    where
        Self: 'a;

    fn borrow(&self) -> &T;
}

impl<T> NestedBorrow<T> for T {
    type Content<'a> = T
    where
        Self: 'a;

    fn borrow(&self) -> &T {
        self
    }
}

impl<T> NestedBorrow<T> for &T {
    type Content<'a> = T
    where
        Self: 'a;

    fn borrow(&self) -> &T {
        &**self
    }
}

#[derive(Clone, PartialEq, Eq, Hash, Debug, Serialize)]
pub struct OffsetMod<'a, C: NestedBorrow<ChemicalComposition<'a>> + 'a = ChemicalComposition<'a>> {
    kind: OffsetKind,
    composition: C::Content<'a>,
}

But that leaks NestedBorrow into the public API for OffsetMod (see https://stackoverflow.com/a/66369912), caused problems with &ChemicalComposition<'_> not being accepted by OffsetKind — it was always looking for the owned ChemicalComposition<'_> version, something I never figured out — and it's generally revolting.

What do you think, is it possible to do better than the type aliases? Is there some Rust pattern that's better suited for structs that sometimes own, and sometimes borrow data — whilst keeping my impl blocks unified with impl<T: Borrow<ChemicalComposition<'a>>> OffsetMod<'a, T>?

0

There are 0 best solutions below