Negative trait bound trick

I remember once seeing a trick to emulate a negative trait bound. Using a Boolean trait and some other machinery. But I forgot how to do it.

I want to be able to

struct Struct<T>(T);
impl Struct<()> {
    fn foo1(&self) -> u8 { 1 }
}

impl<T> Struct<T> {
    fn foo2(&self) -> String { String::new() }
}

fn bar<T>(s1: Struct<()>, s2: Struct<T>) {
    s1.foo1(); // s1 should only call foo1() but not foo2()
    s2.foo2(); // s2 should only call foo2() but not foo1()
}

With the above, Struct<T> impl catches all, and overrides Struct<()> impl. If specificity is provided s1: Struct<()> I would like to restrict available methods to the associated impl. I figure if I can impl EmptyTuple for the type (), maybe I can emulate restricting impl<T: !EmptyTuple> Struct<T> somehow.

It's confusing by saying "overrides", since the only method foo2 is not overrided.

It seems you don't want to let Struct<()> have the foo2 method? And foo1 is irrelevant for your problem?

I don't want to let Struct<()> have foo2 and I don't want every other type for Struct<T> have the foo1 method.

Struct<()>::foo1(todo!()) compiles
Struct<()>::foo2(todo!()) should not compile

Struct<T>::foo1(todo!()) should not compile
Struct<T>::foo2(todo!()) compiles

I once saw a trick vaguely akin to impl<T: IsType> Struct<T> {} and impl<T: !IsType> Struct<T> {} to accomplish this. Without the actual unstable negative impl sytax.

You can use the trick that inherent methods are prioritized in case of duplicate names: Rust Playground

Update the playground link to show the pitfall: it doesn't work for generic inherent wrapper methods

1 Like

On mobile and can't tackle your specific use case right now, but it's this the said trick?

2 Likes

@vague While technically that works for the example I gave. I have underestimated the requirements by over-simplifying the example. Given this code, that "erases" the type in a struct member, it no longer works the same.

struct Struct<T>(T);

pub trait NotForUnit {
    fn foo(&self) {}
}

impl<T> NotForUnit for Struct<T> {
    fn foo(&self) {
        println!("not for ()")
    }
}

impl Struct<()> {
    pub fn foo(&self) {
        println!("() only")
    }
}

struct Store<T>(Struct<T>);
impl<T> Store<T> {
    fn bar(&self) {
        self.0.foo();
    }
}

fn main {
    let store = Store(Struct(()));
    store.bar(); // prints not for ()
}

Though I might still make use of it in cases where I can specify an exact type in a struct member.

@quinedot I believe so because I remember associated types being used to limit to either or cases. I'll have a deeper look at the playground code and verify. Doesn't compile on nightly but is probably because typos due to being typed on mobile?

I pulled it out of my bookmarks (hence the old edition); the compile error is demonstrating the guard. See the comments on what to do to make it compile.

1 Like

Yes, it is a pitfall. I've updated the link.

1 Like

The playground I linked above, when applied directly to your OP, may look like so. It has the downside of needing to provide an implementation of Permission for any T you want to work though, so it's not really much better than a normal trait bound (it just guarantees that won't happen for things you explicitly Deny).

Not sure if there's a way around that without specialization.

1 Like

You're right, this requires specialization to allow an additional fallback catch-all implementation

impl<T> Permission for T {
    type Execute = Allow;
}

conflicting implementations of trait Permission for type ()

When I looked at your earlier playground example, I came up with the same solution you just posted. But got stuck on an important problem:

fn bar<T: Permission<Execute = Allow>>(s1: Struct<()>, s2: Struct<T>, s3: Struct<u8>) {
    s1.foo1(); // compiles as intended
    s2.foo2(); // compiles as intended
    
    s3.foo2(); // the trait bound `u8: Permission` is not satisfied
}

I won't be able to deal with types like Struct<u8>. I think the logic I need, that is the explicit exclusion of one or more types from the universe of all types, is something the compiler can't do right now.

With the current solution I have to explicitly "whitelist" types to be allowed into the generic impl for foo2. Removing the constraint where T: Permission<Execute = Allow> will open the door back for s1.foo2(); to be callable. Whitelist vs blacklist problem.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.