Optionally supporting an operation in a Trait

I was just tinkering with a trait that has two methods:

  1. The first method encodes whether or not some operation is supported. It is cheap to call.
  2. If the operation is supported, we should be able to call the second method at a later point in time (some checks happen when the operation is supported, and before actually executing the operation). I wanted to avoid having an Option return type for this second method, since we should already know from the first method whether the operation is supported or not, and will only be calling this second method if the operation is supported.

Using dependent types, we could just change the return type based on the value returned by the first method. Since that is not a possibility here, I tried to encode something similar by using Infallible:

enum IsSupported<Token> {
    NotSupported,
    Supported(Token)
}

impl<Token: Copy> IsSupported<Token> {
    fn unwrap(&self) -> Token {
        use IsSupported::*;
        match self {
            NotSupported => panic!("bad unwrap"),
            Supported(t) => *t
        }
    }
}

trait HasSupport where Self: Sized {
    fn is_supported() -> IsSupported<Self>;    
}
impl HasSupport for () {
    fn is_supported() -> IsSupported<Self> { IsSupported::Supported(()) }
}
impl HasSupport for std::convert::Infallible {
    fn is_supported() -> IsSupported<Self> { IsSupported::NotSupported }
}

trait WithSupport {
    type SupportThing: HasSupport;
    
    fn has_support() -> IsSupported<Self::SupportThing> {
        <Self as WithSupport>::SupportThing::is_supported()
    }
    
    // `String` is a more complex type in my application, that cannot sensibly be constructed if the operation is not supported.
    fn get_thing(&self, h : Self::SupportThing) -> String;
}

struct X;
impl WithSupport for X {
    type SupportThing = std::convert::Infallible;
    fn get_thing(&self, h: Self::SupportThing) -> String {
        match h {}
    }
}

struct Y;
impl WithSupport for Y {
    type SupportThing = ();
    fn get_thing(&self, h: Self::SupportThing) -> String {
        "this is the thing".to_string()
    }
}

pub fn main() {
    let y = Y;
    let supported = Y::has_support();
    println!("{}", y.get_thing(supported.unwrap()));
    
    let x = X;
    let supported = X::has_support();
    assert!(matches!(supported, IsSupported::NotSupported));
}

The aforementioned trait is called WithSupport here, and IsSupported is basically Option<Token> for now. We check support through has_support, and if that returns an appropriate token, then we can retrieve the supported thing using get_thing at a later point in time. We use Infallible to avoid having to implement get_thing in cases where the operation is not supported.

I was wondering if this is an existing pattern to do this. I haven’t really seen this before or wouldn’t know what to call it, and I might be overcomplicating things.

I don’t know your use case, but, if possible, I’d use trait bounds to implement optional behavior within a trait:

https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=4e8fe3313aabbf2bb20e5bea85a81e4d

In my case, I have a generic context where I have a type T that implements your MaybeDoFoo trait (i.e. my WithSupport trait). I do not know ahead of time whether operation foo (get_thing) is supported, and I hence need to branch based on the question “does T implement SupportsFoo(IsSupported)"? So my SupportsThing is a reified version of the SupportsFoo trait bound you propose; unfortunately, I cannot inspect this trait bound to decide what to do in my generic code, and I do not want to make the return type of do_foo an Option, because I want to know whether the operation is supported separately from actually executing the operation.

Yep, returning a value that encodes a capability of some sort is a not-uncommon pattern. But you might want to consider just using Result instead of a custom enum. It’s idiomatic and lets you represent all three of “always-supported”, “never-supported”, and “maybe-supported”, depending on the type parameters.

2 Likes

I feel like it is similar to the IDET pattern, if your second operation is dyn-compatible. in short, you use a separate trait for the "optional" method. see:

2 Likes

Interesting read, thanks for the reference! I think solution #4 is somewhat similar in spirit to what I’m proposing; I encode extensions being (un)supported using associated types (and use Infallible to allow implementations to not implement certain extensions), whereas your reference uses extension traits and an Option type instead.

It feels like there’s a mapping between both techniques (for dyn-compatible traits, that is); similar to the author’s either_a_or_b, my technique can have multiple associated types (SupportsA and SupportsB) to represent multiple possible extensions. We also run into the same limitation, where the article cannot prevent an implementation from switching between Features A and B across different invocations. Similarly, I cannot prevent an implementation from setting both SupportsA and SupportsB to something non-Infallible.

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.