Determine if trait function's default was overwritten for type

Hello,

A trait of mine contains a function the implementer may overwrite if they desire.
When handling types that implement my trait, I want to know if the user made use of that function, in which case I want to call it, or else want to call a different function with a different signature.

Because the signatures of the two differ, I can't just call the one function in the other.

pub trait Foo{
    fn user_defines_this(&mut self);
    fn user_may_define_this(){
        panic!("This should not be called because it was not overwritten. Call `user_defines_this` instead.");
    }
}

fn bar<T: Foo>(baz: &mut T){
    // what to call? `user_may_define_this` would be good.
}

I still need distinct functions because there are some cases I always call user_defines_this. You can see the second function as an optimized version of the first that works only in some situations.

I see few a options here, and all of them make the trait ugly as other people may use this trait.

  1. Have an associated constant in Foo: const SET_TRUE_IF_YOU_DEFINED_THE_FN: bool = false;

  2. Replace fn user_may_define_this(){ ... } with const USER_MAY_DEFINE_THIS: Option<fn()> = None;. Then check if this constant is Some.

  3. As 2. but not not with an Option but instead with the following. Then check if this constant is not the same function pointer as T::USER_DID_NOT_DEFINE_THIS.

pub trait Foo{
    const USER_MAY_DEFINE_THIS: fn() = Self::USER_DID_NOT_DEFINE_THIS;
    fn user_defines_this(&mut self);
}

trait FooUnset{
    const USER_DID_NOT_DEFINE_THIS: fn() = ||{
        panic!("This should not be called because it was not overwritten. Call `user_defines_this` instead.");
    };
}

impl<T: Foo + ?Sized> FooUnset for T{} //I am not sure if this should be `?Sized` or `Foo: Sized`

Are there any other ideas? The check does not need to be const.

1 Like

In general, don't design interfaces such that they behave this way. You're setting a trap for future maintainers, even though that's probably not your intention. Make the optional method do a right thing when not overridden, instead. More generally, the client of a trait (your baz function) should not care how the implementor of the trait decides to proceed, and should be able to call trait functions as described in the docs for the trait without needing to know anything about the implementation.

For example, you could make it a no-op:

pub trait Foo{
    fn user_defines_this(&mut self);
    fn user_may_define_this() {
    }
}

You may also be able to split the optional method off into a separate trait which is implemented only for types where it's relevant, though the resulting implementation constraints get complicated fast if you do this a lot.

5 Likes

Maybe fn user_may_define_this() -> Result<(), FunctionUndefined> { Err(FunctionUndefined)) } where the Error is pub FunctionUndefined(()); so that it cannot be constructed outside your crate. Then if the function returns Ok you know it was overridden and on Err you know it wasn't.

2 Likes

Thank you for your advice. I don't see how to do that yet with my current design as I can call only one or the two, but it is worth trying for sake of maintainability.

That might be an idea too, though here I only find out if the function was defined if I call it. Ideally I can determine it before that.

Your problem reminds me a bit of what I did in mmtkvdb::storable::Storable:

pub unsafe trait Storable: ToOwned {
    /// Does byte representation have fixed length?
    ///
    /// If this constant is `true`, then trait [`StorableConstBytesLen`] should
    /// also be implemented.
    const CONST_BYTES_LEN: bool;
    /// Does [`Storable::cmp_bytes_unchecked`] perform a trivial (byte wise)
    /// lexicographical comparison?
    const TRIVIAL_CMP: bool = false;

    /// […]

    /// Compares byte representation
    unsafe fn cmp_bytes_unchecked(a: &[u8], b: &[u8]) -> Ordering;
}

I decided to not use a default implementation and make the implementor set the relevant constants manually. However, the scenario might be a bit different. Storable is inherently unsafe, and I (have to) assume that an implementor knows what they are doing and that they set the constants correctly (as well as providing consistent/correct implementations for the methods), even though TRIVIAL_CMP = false is a safe fallback here.

I decided against a default implementation. And require the implementor to make all choices explicitly (except for where a safe default can be provided).

However, I provided this for convenience:

pub unsafe trait Storable: ToOwned {
    /// […]

    /// Compares byte representation using [`Ord`]
    ///
    /// This function is provided for convenient implementation of
    /// [`Storable::cmp_bytes_unchecked`] where desired.
    unsafe fn cmp_bytes_by_ord_unchecked(a: &[u8], b: &[u8]) -> Ordering
    where
        Self: Ord,
    {
        if Self::TRIVIAL_CMP {
            a.cmp(b)
        } else {
            Self::from_bytes_unchecked(a).cmp(&Self::from_bytes_unchecked(b))
        }
    }    /// Compares byte representation
    unsafe fn cmp_bytes_unchecked(a: &[u8], b: &[u8]) -> Ordering;
    }
}

So an implementor may implement cmp_bytes_unchecked by forwarding to cmp_bytes_by_ord_unchecked. Maybe I should have made a function instead of a trait method, but since Storable is not object safe, I thought it didn't matter and might be more convenient as a method

Interesting input, thanks for sharing! Though yes, with yours being unsafe and making the implementor deal with the inner workings of your application I think I don't want to go down that road.

I currently experiment with having multiple traits like suggested by @derspiny.

Though I might have two of such optional methods which would make it confusing again, as I'd had a ImplementedNeither, ImplementedFirst, ImplementedSecond and ImplementedBoth with the former three blanket-impl the last that'd be my bound of the client. :melting_face:

Note that your approach makes this fail at run-time rather than at compile-time:

fn main() {
    struct U;
    impl Foo1 for U {
        fn base_implement(t: &mut Self) {}
    }
    U::extra_implement(); // panics at runtime instead of raising a compiler error
}

(Playground)

1 Like

Right, that is the downside of it. It would be no issue for how I make use of implementing types but maybe the user wants to use them too, then I'd need to point that out in the method docs.

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.