1

High level requirement: I want to expose a single function to the users of my library. Something like:

pub fn execute<T>(data: T, param: String){
    // Some computation here
}

Here is a MRE, with comments, on what exactly I am trying to do:

use std::{sync::{RwLock, Arc}, collections::HashMap};
use dashmap;

/// This function takes a standard DataSet
/// Param just simulates a parameter
fn execute_standard<DS: DataSet + ?Sized>(data: &DS, param: String) -> String {
    return data.get_name().clone()
}

/// execute_cacheable is exactly the same as execute_standard
/// except it's arg is CacheableDataSet
/// Arc to share across threads  
/// Perhaps should be & instead of Arc
fn execute_cacheable<DS: CacheableDataSet + ?Sized>(data: &DS, param: String) -> String {
    // Simulating some fancy cache lookup
    let cache = data.get_cache();
    let lookup = cache.get(&param);
    let res = if let Some(r) = lookup
    {
        println!("Found: {:?}", r);
        r.clone()
    } else {
        // Not found
        let r = execute_standard(&*data, param.clone());
        cache.insert(param, r.clone());
        r
    };
    return res
}

/// No cache
struct Standard {
    pub name: String
}

trait DataSet {
    /// Simulates one of the methods on the main trait
    fn get_name(&self) -> &String;
}

impl DataSet for Standard {
    fn get_name(&self) -> &String {&self.name}
}

/// This Struct has Cache
struct WithCache {
    pub name: String,
    pub cache: dashmap::DashMap<String, String>
}

trait CacheableDataSet: DataSet {
    fn get_cache(&self) -> &dashmap::DashMap<String, String>;
}

impl DataSet for WithCache {
    fn get_name(&self) -> &String {&self.name}
}

impl CacheableDataSet for WithCache {
    fn get_cache(&self) -> &dashmap::DashMap<String, String> {&self.cache}
}

pub fn main() {
    // Arc is important because I share objects across threads (using actix)
    let a = Arc::new(Standard{name: "London".into()});
    let b = Arc::new(WithCache{name: "NY".into(), cache: dashmap::DashMap::default()});

    // I'd like the users to use a single execute() function
    
    // execute(&a, String::from("X"))
    // execute(&b, String::from("X"))

    // I wouldn't mind if it was like this
    // a.execute(String::from("X"))
    // b.execute(String::from("X"))

    // What I can do now is not good enough I think
    println!("{}", execute_standard(&*b, String::from("X")));
    println!("{}", execute_cacheable(&*b, String::from("X")));
}

Note that trait DataSet contains several methods, some default and some not. As such to avoid duplication it would be best to "reuse" what's already in the DataSet.

It doesn't seem like a good idea to me to expose two functions to the users.

Originally I was looking for a way to check trait implementation at runtime. I found this and this but it uses unstable features and I'd like to avoid that as much as possible.

One possible solution I see is to use feature gates. Implement all Cacheable methods inside DataSet with #[cfg(feature = "cache")] flag and use if cfg!(feature = "cache") inside execute, which then would have same signature as execute_standard.

Surely I am not the first person trying to achieve this :) Is there any better way?

Anatoly Bugakov
  • 772
  • 1
  • 7
  • 18
  • Something like this maybe? https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=ef0c23d1734abfe5f5aac7a69a0bfe77 – PitaJ Feb 17 '23 at 22:21
  • Thank you very much @PitaJ , I will explore this approach. The only thing, since CacheableDataSet is "a subset" of DataSet, I'd do something like `trait CacheableDataSet: DataSet {...}` – Anatoly Bugakov Feb 17 '23 at 22:29
  • You might run into conflicts with `impl DataSet for T` if you make `CacheableDataSet` a supertrait of `DataSet`. – PitaJ Feb 17 '23 at 22:32
  • Specifically, if you have any non-default methods on `DataSet`, this won't be possible. So you will either need to move any common non-default methods into a third trait or NOT make it a supertrait and duplicate all the non-default methods. – PitaJ Feb 17 '23 at 22:39
  • @PitaJ, many thanks. Indeed I ran into a problem with other methods of `DataSet` not implemented for `CacheableDataSet`. (there are quite a few). Would you be able hint me at how it could be solved with another (third) trait? – Anatoly Bugakov Feb 18 '23 at 14:13
  • @PitaJ, to make things more complicated, `execute_cacheable` signature is actually `execute_cacheabledata: Arc>` (I should've mentioned it straight away in the question). Arc because it is shared across threads and RwLock to allow read and Write into cache. – Anatoly Bugakov Feb 18 '23 at 15:20
  • Please provide a minimal reproducible example with the full traits, types, etc you are using. – PitaJ Feb 18 '23 at 17:24
  • @PitaJ, my apologies. I thought it would be a simple one. I have ammended the question with an MRE of what I am trying to achieve. – Anatoly Bugakov Feb 18 '23 at 18:53

2 Answers2

1

Okay so based on your MRE, I have two options.

  1. Two traits: DataSet and CacheableDataSet, identical methods in each except for execute and get_cache
trait DataSet {
    /// Simulates one of the methods on the main trait
    fn get_name(&self) -> &String;

    fn execute(&self, param: String) -> String {
        execute_standard(self, param)
    }
}

trait CacheableDataSet {
    /// Simulates one of the methods on the main trait
    fn get_name(&self) -> &String;

    fn get_cache(&self) -> &dashmap::DashMap<String, String>;
}

impl<T: CacheableDataSet + ?Sized> DataSet for T {
    fn get_name(&self) -> &String {
        CacheableDataSet::get_name(self)
    }

    fn execute(&self, param: String) -> String {
        execute_cacheable(self, param)
    }
}

/// No cache
struct Standard {
    pub name: String,
}

impl DataSet for Standard {
    fn get_name(&self) -> &String {
        &self.name
    }
}

/// This Struct has Cache
struct WithCache {
    pub name: String,
    pub cache: dashmap::DashMap<String, String>,
}

impl CacheableDataSet for WithCache {
    fn get_name(&self) -> &String {
        &self.name
    }
    
    fn get_cache(&self) -> &dashmap::DashMap<String, String> {
        &self.cache
    }
}

playground

  1. Three traits: DataSet with all shared methods, DataSetX for execute, and CacheableDataSetX for get_cache
trait DataSetShared {
    /// Simulates one of the methods on the main trait
    fn get_name(&self) -> &String;
}

trait DataSet: DataSetShared {
    fn execute(&self, param: String) -> String {
        execute_standard(self, param)
    }
}

trait CacheableDataSet: DataSetShared {
    fn get_cache(&self) -> &dashmap::DashMap<String, String>;
}

impl<T: CacheableDataSet + ?Sized> DataSet for T {
    fn execute(&self, param: String) -> String {
        execute_cacheable(self, param)
    }
}

/// No cache
struct Standard {
    pub name: String,
}

impl DataSetShared for Standard {
    fn get_name(&self) -> &String {
        &self.name
    }
}
impl DataSet for Standard {} // Needed to enable `execute`

/// This Struct has Cache
struct WithCache {
    pub name: String,
    pub cache: dashmap::DashMap<String, String>,
}

impl DataSetShared for WithCache {
    fn get_name(&self) -> &String {
        &self.name
    }
}
impl CacheableDataSet for WithCache {
    fn get_cache(&self) -> &dashmap::DashMap<String, String> {
        &self.cache
    }
}

playground

Personally, I prefer #1 because one only needs to implement a single trait for each type of DataSet.

PitaJ
  • 12,969
  • 6
  • 36
  • 55
0

I think, what you seek is something akin to function overloading. To my knowledge, Rust does not support that (just like in C). However, Rust's typesystem may be made to work for us (without requiring something like if constexpr or SFINAE in C++) like this:

fn main() {
    let ds = Dataset {};
    let cached_ds = CachaeableDataset {};

    execute(&ds);

    execute(&cached_ds);
}

trait IDataset {
    fn compute(&self);
}

trait ICacheable {
    fn compute_cached(&self);
}

struct Dataset {}

struct CachaeableDataset {}

impl IDataset for Dataset {
    fn compute(&self) {
        // Compute the results directly, without using any cache.
        println!("This is direct computation.");
    }
}

impl IDataset for CachaeableDataset {
    fn compute(&self) {
        // First check the cache, and then, if needed, compute directly
        self.compute_cached();
        println!("This is direct computation called after checking the cache.");
    }
}

impl ICacheable for CachaeableDataset {
    fn compute_cached(&self) {
        println!("This is cached computation.");
    }
}

fn execute<T: IDataset>(dataset: &T) {
    dataset.compute();
}
Arnab De
  • 402
  • 4
  • 12