Making a value of a type `Undroppable` at compile time

Ah yeah, assert_type! won't capture generics. To bridge from real const generics, typenum provides the generic_const_mappings module that allows you to write U<N> to go from const generic to typenum, but note that you're still not allowed to project from a const generic N in type bounds. You can sometimes manage to get things to work with just bounds directly on the generics, but the sooner you get everything into the typenum world the easier it is.

I was (a bit nerd sniped and thus) able to get to an I think working solution: [playground]

pub trait ExposeSecret<'max, T, MEC: Unsigned, EC: Unsigned>: Sized {
    type Exposed<'brand>
    where
        'max: 'brand;

    type Next: ExposeSecret<'max, T, MEC, op![EC + U1]>
    where
        EC: Add<U1>,
        op![EC + U1]: Unsigned;

    fn expose_secret<F, R>(self, scope: F) -> (Self::Next, R)
    where
        EC: Add<U1> + IsLess<MEC, Output = True>,
        op![EC + U1]: Unsigned,
        F: for<'brand> FnOnce(Self::Exposed<'brand>) -> R;
}

impl<'a, T: Zeroize, MEC: Unsigned, EC: Unsigned> ExposeSecret<'a, &'a T, MEC, EC>
    for Secret<T, MEC, EC>
{
    type Exposed<'brand> = Exposed<'brand, &'brand T>
    where
        'a: 'brand;

    type Next = Secret<T, MEC, op![EC + U1]>
    where
        EC: Add<U1>,
        op![EC + U1]: Unsigned;

    fn expose_secret<F, R>(self, scope: F) -> (Self::Next, R)
    where
        EC: Add<U1> + IsLess<MEC, Output = True>,
        op![EC + U1]: Unsigned,
        F: for<'brand> FnOnce(Exposed<'brand, &'brand T>) -> R,
    {
        let result = scope(Exposed(&self.0, PhantomData));
        (Secret(self.0, PhantomData), result)
    }
}

It's tricky to write because there's a lot of subtle interactions of things like implied bounds involved. E.g. if I switch the impl to use FnOnce(Self::Exposed<'brand>) instead, which you might think would be equal, we get the unhelpful error of just:

error[E0478]: lifetime bound not satisfied
  --> src/lib.rs:39:8
   |
39 |     fn expose_secret<F, R>(self, scope: F) -> (Self::Next, R)
   |        ^^^^^^^^^^^^^
   |
note: lifetime parameter instantiated with the lifetime `'a` as defined here
  --> src/lib.rs:27:6
   |
27 | impl<'a, T: Zeroize, MEC: Unsigned, EC: Unsigned> ExposeSecret<'a, &'a T, MEC, EC>
   |      ^^

When running into "bleeding edge" features with subpar errors, the only (and necessary) trick is to simplify. Thus not using impl Trait here. E.g. while getting to this impl, I hit that error and entirely commented out the expose_secret function as a reduced test, and then got shown a different error which needed to be fixed first for any hope of fixing the method to actually work.

But with that written, I need to ask the question: do you need to make this a trait? Unless you're a) going to have more than one implementation of the trait and b) going to write code generic over the trait to work on all of such types, you shouldn't be using a trait; using traits where they aren't necessary just serves to make things more complicated for no benefit. See also matklad on concrete abstraction as a code smell.

1 Like