Help constraining to a Trait with associated type

I am trying to implement an implementation agnostic data store, but I am having an issue around how to constrain a Trait with its associated type.

Here's the relevant code:

mod instance;
mod persistence;
mod state;
mod workflow;

use async_trait::async_trait;
use futures::stream::TryStreamExt;
use serde::de::DeserializeOwned;
use serde::Serialize;

pub use self::instance::Instance;
pub use self::workflow::Workflow;

pub struct Store {
    pub workflows: Box<dyn Record<T = Workflow>>,
    pub instances: Box<dyn Record<T = Instance>>,
}

impl Store {
    pub async fn new<R: Record>() -> anyhow::Result<Self> {
        let workflows: Box<dyn Record<T = Workflow>> = Box::new(R::new("workflows").await?);
        let instances: Box<dyn Record<T = Instance>> = Box::new(R::new("instances").await?);
        Ok(Self {
            workflows,
            instances,
        })
    }
}

#[async_trait]
pub trait Record {
    type T: Serialize + DeserializeOwned + Send + Sync + Unpin;

    async fn new(name: &str) -> anyhow::Result<Self>
    where
        Self: Sized;
    async fn save(&self, record: &Self::T) -> anyhow::Result<()>;
    async fn load(&self) -> anyhow::Result<Option<Self::T>>;
}

pub struct MongoDbRecord<R>
where
    R: Serialize + DeserializeOwned + Send + Sync + Unpin,
{
    collection: mongodb::Collection<R>,
}

#[async_trait]
impl<R> Record for MongoDbRecord<R>
where
    R: Serialize + DeserializeOwned + Send + Sync + Unpin,
{
    type T = R;

    async fn new(record_name: &str) -> anyhow::Result<Self> {
        let client = mongodb::Client::with_uri_str("mongodb://localhost:27017/mflow").await?;
        let db = client.default_database().unwrap();
        let collection = db.collection::<R>(record_name);
        Ok(Self { collection })
    }

    async fn save(&self, record: &Self::T) -> anyhow::Result<()> {
        self.collection.insert_one(record, None).await?;
        Ok(())
    }

    async fn load(&self) -> anyhow::Result<Option<Self::T>> {
        let mut cursor = self.collection.find(None, None).await?;
        if let Some(result) = cursor.try_next().await? {
            Ok(Some(result))
        } else {
            Ok(None)
        }
    }
}

The error I am getting now is on the lines:

let workflows: Box<dyn Record<T = Workflow>> = Box::new(R::new("workflows").await?);
let instances: Box<dyn Record<T = Instance>> = Box::new(R::new("instances").await?);

And here's the error:

error[E0271]: type mismatch resolving `<R as Record>::T == Workflow`
  --> src/store/mod.rs:24:24
   |
24 |             workflows: Box::new(workflows),
   |                        ^^^^^^^^^^^^^^^^^^^ expected struct `Workflow`, found associated type
   |
   = note:       expected struct `Workflow`
           found associated type `<R as Record>::T`
   = help: consider constraining the associated type `<R as Record>::T` to `Workflow`
   = note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
   = note: required for the cast from `R` to the object type `dyn Record<T = Workflow>`

error[E0271]: type mismatch resolving `<R as Record>::T == Instance`
  --> src/store/mod.rs:25:24
   |
25 |             instances: Box::new(instances),
   |                        ^^^^^^^^^^^^^^^^^^^ expected struct `Instance`, found associated type
   |
   = note:       expected struct `Instance`
           found associated type `<R as Record>::T`
   = help: consider constraining the associated type `<R as Record>::T` to `Instance`
   = note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html
   = note: required for the cast from `R` to the object type `dyn Record<T = Instance>`

This code doesn't make sense as is. You're trying to pass R to one place that requires Record::T to be Workflow and one that requires it to be Instance. You can constrain R::T to be either one of them in the function signature, but not both.

2 Likes

Thank you so much for the insight, super helpful as always <3

But yes, I think that's where I could use some guidance in case anyone have the time to expand this further.

My goal with the code I shared is to have an abstraction that knows how to persist any generic Serializable struct.

This is what I am (assumedly badly) calling Record. This "trait" (or whatever fits best in this case) responsibility is to be generic over a Serializable trait but to know how to persist it.

Now the other layer I am trying to add is what I am calling a Store. A Store is generic over the persistence implementation (aka Record above), but concretely apply the given (meaning, user-defined) persistence implementation to a known set of Serializables that are then exposed.

The goal here is to allow Store to be passed around (maybe as a Trait?) and delegate the persistence implementation to the user, in a way that I can mock this part to implement tests, or offer different persistence strategies to different use cases. It's almost like a more complex Strategy Design Pattern.

Would anyone point me in the right direction to achieve this level of indirection/abstraction?

Also let me know if this doesn't make any sense, which may be likely too :slight_smile:

Thanks!

Ahh so you want a single type R to be able to serialize and deserialize both Workflow and Instance? In that case you just need Record<T> instead of using an associated type.

#[async_trait]
pub trait Record<T>
where
    T: Serialize + DeserializeOwned + Send + Sync + Unpin,
{
    async fn new(name: &str) -> anyhow::Result<Self>
    where
        Self: Sized;
    async fn save(&self, record: &T) -> anyhow::Result<()>;
    async fn load(&self) -> anyhow::Result<Option<T>>;
}

pub struct Store {
    pub workflows: Box<dyn Record<Workflow>>,
    pub instances: Box<dyn Record<Instance>>,
}

impl Store {
    pub async fn new<R: Record<Workflow> + Record<Instance> + 'static>() -> anyhow::Result<Self> {
        let workflows: Box<dyn Record<Workflow>> =
            Box::new(<R as Record<Workflow>>::new("workflows").await?);
        let instances: Box<dyn Record<Instance>> =
            Box::new(<R as Record<Instance>>::new("instances").await?);
        Ok(Self {
            workflows,
            instances,
        })
    }
}

Associated types are sort of like implementation details between a trait and the type implementing it, so there can only ever be a single concrete associated type for a single type implementing the trait. But a single type can implement a trait with a type parameter for multiple types, which it seems like is the thing you are trying to accomplish.

1 Like

Holy crap, I had no idea this was possible. Haven't tried the code yet but will let you know once I do. Thank you!

One additional question when you can: how would I call Store's new method, passing that union of 2 records?

EDIT: Adding the code as it stands right now:

mod instance;
mod persistence;
mod state;
mod workflow;

use async_trait::async_trait;
use futures::stream::TryStreamExt;
use serde::de::DeserializeOwned;
use serde::Serialize;

pub use self::instance::Instance;
pub use self::workflow::Workflow;

#[async_trait]
pub trait PersistenceStrategy<T>
where
    T: Serialize + DeserializeOwned + Send + Sync + Unpin,
{
    async fn new(name: &str) -> anyhow::Result<Self>
    where
        Self: Sized;
    async fn save(&self, record: &T) -> anyhow::Result<()>;
    async fn load(&self) -> anyhow::Result<Option<T>>;
}

pub struct Store {
    pub workflows: Box<dyn PersistenceStrategy<Workflow>>,
    pub instances: Box<dyn PersistenceStrategy<Instance>>,
}

impl Store {
    pub async fn new<R: PersistenceStrategy<Workflow> + PersistenceStrategy<Instance> + 'static>(
    ) -> anyhow::Result<Self> {
        let workflows: Box<dyn PersistenceStrategy<Workflow>> =
            Box::new(<R as PersistenceStrategy<Workflow>>::new("workflows").await?);
        let instances: Box<dyn PersistenceStrategy<Instance>> =
            Box::new(<R as PersistenceStrategy<Instance>>::new("instances").await?);
        Ok(Self {
            workflows,
            instances,
        })
    }
}

pub struct MongoDbPersistence<R>
where
    R: Serialize + DeserializeOwned + Send + Sync + Unpin,
{
    collection: mongodb::Collection<R>,
}

#[async_trait]
impl<R> PersistenceStrategy<R> for MongoDbPersistence<R>
where
    R: Serialize + DeserializeOwned + Send + Sync + Unpin,
{
    async fn new(record_name: &str) -> anyhow::Result<Self> {
        let client = mongodb::Client::with_uri_str("mongodb://localhost:27017/mflow").await?;
        let db = client.default_database().unwrap();
        let collection = db.collection::<R>(record_name);
        Ok(Self { collection })
    }

    async fn save(&self, record: &R) -> anyhow::Result<()> {
        self.collection.insert_one(record, None).await?;
        Ok(())
    }

    async fn load(&self) -> anyhow::Result<Option<R>> {
        let mut cursor = self.collection.find(None, None).await?;
        if let Some(result) = cursor.try_next().await? {
            Ok(Some(result))
        } else {
            Ok(None)
        }
    }
}

And what's puzzling me is how I can instantiate Store for a given concrete implementation of PersistenceStrategy.

This works:

let workflows = MongoDbPersistence::<Workflow>::new("workflows").await?;
let instances = MongoDbPersistence::<Instance>::new("instances").await?;

But not sure how to take it further:

let store = Store::new::<MongoDbPersistence<Workflow>>().await?;

Obviously generates the following errors:

error[E0277]: the trait bound `MongoDbPersistence<workflow::workflow::Workflow>: PersistenceStrategy<store::workflow::Workflow>` is not satisfied
  --> src/main.rs:31:17
   |
31 |     let store = Store::new::<MongoDbPersistence<Workflow>>().await?;
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PersistenceStrategy<store::workflow::Workflow>` is not implemented for `MongoDbPersistence<workflow::workflow::Workflow>`
   |
   = help: the trait `PersistenceStrategy<R>` is implemented for `MongoDbPersistence<R>`
note: required by a bound in `Store::new`
  --> src/store/mod.rs:32:25
   |
32 |     pub async fn new<R: PersistenceStrategy<Workflow> + PersistenceStrategy<Instance> + 'static>(
   |                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Store::new`

error[E0277]: the trait bound `MongoDbPersistence<workflow::workflow::Workflow>: PersistenceStrategy<store::instance::Instance>` is not satisfied
  --> src/main.rs:31:17
   |
31 |     let store = Store::new::<MongoDbPersistence<Workflow>>().await?;
   |                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `PersistenceStrategy<store::instance::Instance>` is not implemented for `MongoDbPersistence<workflow::workflow::Workflow>`
   |
   = help: the trait `PersistenceStrategy<R>` is implemented for `MongoDbPersistence<R>`
note: required by a bound in `Store::new`
  --> src/store/mod.rs:32:57
   |
32 |     pub async fn new<R: PersistenceStrategy<Workflow> + PersistenceStrategy<Instance> + 'static>(
   |                                                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `Store::new`

For more information about this error, try `rustc --explain E0277`.

I am not sure how to instantiate a Store that implements the union of the traits above.

Ahh yep I skimmed right over the impl part, that does complicate things a bit since you don't have a single type that actually implements both traits.

Here's a sort of goofy option

pub struct MongoProvider;

impl RecordProvider for MongoProvider {
    type Workflow = MongoDbRecord<Workflow>;
    type Instance = MongoDbRecord<Instance>;
}

pub trait RecordProvider {
    type Workflow: Record<Workflow> + 'static;
    type Instance: Record<Instance> + 'static;
}

impl Store {
    pub async fn new<R: RecordProvider>() -> anyhow::Result<Self> {
        let workflows: Box<dyn Record<Workflow>> = Box::new(R::Workflow::new("workflows").await?);
        let instances: Box<dyn Record<Instance>> = Box::new(R::Instance::new("instances").await?);
        Ok(Self {
            workflows,
            instances,
        })
    }
}

You could probably also use GATs to avoid having a trait that just lists out all the specific instantiations of the type too.

Gonna look into GATs next, but for the time being having the Provider abstraction was exactly what I didn't know how to achieve. Thank you, thank you, thank you!

Ended up adding a test that implements an InMemoryProvider and it worked flawlessly. Here's what it looks like for completeness of the thread:

mod instance;
mod mongo;
mod persistence;
mod state;
mod workflow;

use async_trait::async_trait;
use serde::de::DeserializeOwned;
use serde::Serialize;

pub use self::instance::Instance;
pub use self::mongo::MongoProvider;
pub use self::workflow::Workflow;

pub struct Store {
    pub workflows: Box<dyn PersistenceStrategy<Workflow>>,
    pub instances: Box<dyn PersistenceStrategy<Instance>>,
}

#[async_trait]
pub trait PersistenceStrategy<T>
where
    T: Serialize + DeserializeOwned + Send + Sync + Unpin + Clone,
{
    async fn new(name: &str) -> anyhow::Result<Self>
    where
        Self: Sized;
    async fn save(&mut self, record: &T) -> anyhow::Result<()>;
    async fn load(&self) -> anyhow::Result<Option<T>>;
}

pub trait PersistenceStrategyProvider {
    type Workflow: PersistenceStrategy<Workflow> + 'static;
    type Instance: PersistenceStrategy<Instance> + 'static;
}

impl Store {
    pub async fn new<R: PersistenceStrategyProvider>() -> anyhow::Result<Self> {
        let workflows: Box<dyn PersistenceStrategy<Workflow>> =
            Box::new(R::Workflow::new("workflows").await?);
        let instances: Box<dyn PersistenceStrategy<Instance>> =
            Box::new(R::Instance::new("instances").await?);
        Ok(Self {
            workflows,
            instances,
        })
    }
}

#[cfg(test)]
mod tests {
    use std::fs;

    use super::*;
    use crate::store::workflow::Workflow;
    use serde::Serialize;

    pub struct InMemoryProvider;

    impl PersistenceStrategyProvider for InMemoryProvider {
        type Workflow = InMemoryPersistence<Workflow>;
        type Instance = InMemoryPersistence<Instance>;
    }

    pub struct InMemoryPersistence<R>
    where
        R: Serialize + DeserializeOwned + Send + Sync + Unpin + Clone,
    {
        record: Option<R>,
    }

    #[async_trait]
    impl<R> PersistenceStrategy<R> for InMemoryPersistence<R>
    where
        R: Serialize + DeserializeOwned + Send + Sync + Unpin + Clone,
    {
        async fn new(_record_name: &str) -> anyhow::Result<Self> {
            Ok(Self { record: None })
        }

        async fn save(&mut self, record: &R) -> anyhow::Result<()> {
            self.record = Some(record.clone());
            Ok(())
        }

        async fn load(&self) -> anyhow::Result<Option<R>> {
            Ok(self.record.clone())
        }
    }

    #[tokio::test]
    async fn test_store() -> anyhow::Result<()> {
        let mut store = Store::new::<InMemoryProvider>().await?;
        let str = fs::read_to_string("tests/fixtures/simple.workflow.json")?;
        let workflow: Workflow = serde_json::from_str(&str)?;
        store.workflows.save(&workflow).await?;
        let loaded = store.workflows.load().await?.unwrap();
        assert_eq!(loaded, workflow);
        Ok(())
    }
}

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.