Conditional trait constraints without code duplication

I would like to implement a feature, that if selected requires some constraints on traits and type, e.g. like in

pub trait SomeTrait: FeatureDependendTrait {
    // ... lots of other stuff ...
}

impl<T> SomeGenericFunction<T>
where
    T: FeatureDependendTrait + OtherTraits
{
    // ... lots of other stuff ...
}

The only possibility that came to my mind would be to use feature guards like

#[cfg(feature="my_feature")]
pub trait SomeTrait: FeatureDependendTrait {
    // ... lots of other stuff ...
}

#[cfg(not(feature="my_feature"))]
pub trait SomeTrait {
    // ... lots of other stuff ...
}

#[cfg(feature="my_feature")]
impl<T> SomeGenericFunction<T>
where
    T: FeatureDependendTrait + OtherTraits
{
    // ... lots of other stuff ...
}

#[cfg(not(feature="my_feature"))]
impl<T> SomeGenericFunction<T>
where
    T: FeatureDependendTrait + OtherTraits
{
    // ... lots of other stuff ...
}

Though this solution works, it would also produce a lot of duplicated code which I would like to avoid.
Is there any better way to achieve this?

Generally you should consider the fact that if some code depends on your library and compiles with the feature off, it must also compile with the feature on. This is because the user might depend on two different things, and each of those things depend on your library, but only one of them turns on the feature. In this case, both crates get your crate with the feature on, even if one of them didn't ask for it.

In other words, also the "solution" with code duplication wouldn't work well?

What would be the solution then? I could make all calls to methods of "FeatureDependendTrait" conditional on "my_feature", but would have to check somehow at run-time if a given concrete type implements this feature. Is that possible?

Can you explain a bit more in detail why you need this conditional trait?

The problem is the following: A general solver library uses serialization for checkpointing and storing intermediate results (since the solver could sometimes run for hours or even longer). The problem to be solved is specified by implementing a trait for some user-defined struct.

In another context, someone (me) would like to use this library to solve problems, where the user-defined struct contains a lot of data generated by some other process. The current context would require to copy the data instead of borrowing it, due to the constraint on serde. Checkpointing and intermediate storage on the other hand is not needed.

I'm not sure what that has to do with feature-dependent where clauses?

It sounds like those are different enough use cases to merit including two separate APIs in the crate. (or picking one and sticking to it so you can serve that use case even better)

Perhaps you could focus on the use case that requires the trait, and then make it possible for a user to write some sort of trivial, no-op impl for the trait if they want the other use case. (you could perhaps even provide a utility macro for generating these impls)

Features should only add value to a crate. If there is the possibility that one library requires a feature, and another library specifically requires not having the feature, then it shouldn't be a feature.

Because to enforce that the user provided struct indeed implements the Serialize trait, if this feature is enabled.

Ah. Hm. I didn't realize the trait you were trying to be conditional over was Serialize. So no-op impls for Serialize might not be a good idea. However, you could define a trait which provides functionality specific to your checkpointing:

(note: completely making this up as I go)

pub trait Checkpoint {
    fn has_checkpointing(&self) -> bool;
    fn save_checkpoint(&self, path: impl AsRef<Path>) -> Result<(), Error>;
    fn load_checkpoint(path: impl AsRef<Path>) -> Result<Self, Error>;
}

// example impl for checkpointing
impl Checkpoint for A {
    fn has_checkpointing(&self) -> bool { true }

    fn save_checkpoint(&self, path: impl AsRef<Path>) -> Result<(), Error>
    { /* use serde */ }

    fn load_checkpoint(path: impl AsRef<Path>) -> Result<Self, Error>
    { /* use serde */ }
}

// example no-op impl
impl Checkpoint for B {
    fn has_checkpointing(&self) -> bool { false }

    fn save_checkpoint(&self, path: impl AsRef<Path>) -> Result<(), Error>
    { panic!("type does not support checkpointing") }

    fn load_checkpoint(path: impl AsRef<Path>) -> Result<Self, Error>
    { panic!("type does not support checkpointing") }
}

Notice here that B does not need to implement Serialize. A does, but that's only because it uses serde inside the impl. With a setup like this, you can try to provide tools that focus on making it easy for the user to write these boilerplate impls for their own types.

Yes, factoring out the checkpointing into another trait is probably the best way to go forward, although this would require some more refactoring then I have hoped.

I will suggest this approach to the owner of the library (which is not me).

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.