Beginner(ish) generics question - impl needs "stricter requirement than trait"

I'm trying to implement a key-value SettingsDatabase abstraction for saved app settings. It would have implementations for backends like Apple's "user defaults", Sqlite, Android preferences, etc. It would be able to save different kinds of simple values (i64, bool, lists of String, etc). So I was thinking I would have two traits - SettingsDatabase and SettingsValue.

trait SettingsValue { ?? }
trait SettingsDatabase {
    fn set<V: SettingsValue>(&mut self, key: &str, value: V);
}

My confusion comes when I try to implement this, because within an impl for a SettingsDatabase, I need the V type to have more traits, which would allow me to save it to that specific backend. For example, I tried:

struct AppleSettings {
    // NSUserDefaults is the Apple-specific thing
    ud: Retained<NSUserDefaults>
}
trait UDValue : SettingsValue {
    fn save(ud: &NSUserDefaults, key: Key, value: Self);
}
impl SettingsDatabase for AppleSettings {
    fn set<V: UDValue>(&mut self, key: &str, value: V);
        V::save(self.ud, key, value);        
    }
}
impl UDValue for i64 {
    fn set(&self, ud: &NSUserDefaults, key: &str) {
        // save the int in the user defaults
    }
}

As you can probably guess, it didn't like me changing the generic parameter bound on set:

error[E0276]: impl has stricter requirements than trait
  --> settings/src/apple.rs:64:15
   |
64 |     fn set<V: UDValue>(&mut self, key: Key, value: V) {
   |               ^^^^^^^ impl has extra requirement `V: UDValue`

How does one properly express this? What am I doing here? :slight_smile:

You need to define the trait in a way that lets implementations have bounds on what sort of value they accept. One way would be to make the trait generic over the value type:

trait SettingsDatabase<V> {
    fn set(&mut self, key: &str, value: V);
}

impl<V: UDValue> SettingsDatabase<V> for AppleSettings {...}

However, then you can’t practically use dyn SettingsDatabase, if that was something you were hoping for; it also means that any function generic over T: SettingsDatabase needs to specify which types it is planning to write, which is probably undesirable in this case. Another is to require each implementation to specify a specific enum or other such type for all values:

trait SettingsDatabase {
    type Value;
    fn set(&mut self, key: &str, value: Self::Value);

    // Convenience for conversion
    fn set_from<V: Into<Self::Value>>(&mut self, key: &str, value: V)
    where
        Self: Sized // keeps the trait dyn compatible
    {
        self.set(key, value.into());
    }
}

impl SettingsDatabase for AppleSettings {
    type Value = NSObject; // whatever your type for all possible values is

    fn set(&mut self, key: &str, value: Self::Value) {...}
}

This way, the set of usable value types is defined by the From/Into implementations present.

3 Likes

Thanks, I will try the second one I think and see how it goes. I'm not sure I care about dyn SettingsDatabase since each app will probably use one or the other, depending on some #[cfg(target_os = ...)], and the 2nd example is closer to what I was imagining.

I wonder if it would be possible (in the space of future Rust, or theoretical languages) to have associated traits/type-classes:

trait SettingsDatabase {
    trait Value;   // possibly constrained, like Value: SettingsValue
    fn set<V: Value>(&mut self, key &str, value: V); 

It would definitely be useful to have that, but I’m not aware of any design work on such a feature. There is a sense in which you get some of that power by using an associated type together with a generic trait, as the V: Into<Self::Value> bound does — it says “to be used as a value, V must implement the trait Into<Self::Value>”, so each implementor gets to pick their trait, as long as the trait they pick is a particular instantiation of the generic trait Into. Of course, this isn't as powerful because each implementation of SettingsDatabase only gets to use the into() function and not an arbitrary set of trait functions of its choice, but it’s often possible to get the same effects by implementing whatever custom operations you need on the output of into() (or, generally, the output of the functions of whatever trait you use instead of Into).