Как обрабатывать необязательную черту в подписи функции в стабильной версии?

Требование высокого уровня: я хочу предоставить пользователям моей библиотеки одну функцию. Что-то вроде:

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

Вот MRE с комментариями о том, что именно я пытаюсь сделать:

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")));
}

Обратите внимание, что trait DataSet содержит несколько методов, некоторые по умолчанию, а некоторые нет. Таким образом, чтобы избежать дублирования, было бы лучше «повторно использовать» то, что уже есть в DataSet.

Мне не кажется хорошей идеей предоставлять пользователям две функции.

Первоначально я искал способ проверить реализацию типажа во время выполнения. Я нашел это и это, но они используют нестабильные функции, и я хотел бы избежать этого, насколько это возможно.

Одним из возможных решений, которое я вижу, является использование функциональных шлюзов. Реализуйте все методы Cacheable внутри DataSet с флагом #[cfg(feature = "cache")] и используйте if cfg!(feature = "cache") внутри execute, который тогда будет иметь ту же подпись, что и execute_standard.

Наверняка я не первый, кто пытается этого добиться :) Есть ли лучший способ?

Что-то вроде этого, может быть? play.rust-lang.org/…

PitaJ 17.02.2023 23:21

Большое спасибо @PitaJ, я изучу этот подход. Единственное, поскольку CacheableDataSet является «подмножеством» DataSet, я бы сделал что-то вроде trait CacheableDataSet: DataSet {...}

Anatoly Bugakov 17.02.2023 23:29

У вас могут возникнуть конфликты с impl<T: CacheableDataSet + ?Sized> DataSet for T, если вы сделаете CacheableDataSet суперчертой DataSet.

PitaJ 17.02.2023 23:32

В частности, если у вас есть какие-либо методы не по умолчанию для DataSet, это будет невозможно. Таким образом, вам нужно будет либо переместить любые общие методы не по умолчанию в третью черту, либо НЕ делать ее суперчертой и дублировать все методы не по умолчанию.

PitaJ 17.02.2023 23:39

@PitaJ, большое спасибо. Действительно, я столкнулся с проблемой других методов DataSet, не реализованных для CacheableDataSet. (их довольно много). Не могли бы вы подсказать мне, как это можно решить с помощью другого (третьего) признака?

Anatoly Bugakov 18.02.2023 15:13

@PitaJ, чтобы усложнить ситуацию, execute_cacheable подпись на самом деле execute_cacheable<DS: DataSet + Cacheable + ?Sized>data: Arc<RwLock<DS>> (я должен был сразу упомянуть об этом в вопросе). Arc, потому что он является общим для потоков и RwLock, чтобы разрешить чтение и запись в кеш.

Anatoly Bugakov 18.02.2023 16:20

Предоставьте минимальный воспроизводимый пример с полными чертами, типами и т. д., которые вы используете.

PitaJ 18.02.2023 18:24

@PitaJ, мои извинения. Я думал, что это будет простой. Я исправил вопрос с помощью MRE того, чего я пытаюсь достичь.

Anatoly Bugakov 18.02.2023 19:53
Как создавать пользовательские общие типы в Python (50/100 дней Python)
Как создавать пользовательские общие типы в Python (50/100 дней Python)
Помимо встроенных типов, модуль типизации в Python предоставляет возможность определения общих типов, что позволяет вам определять типы, которые могут...
1
8
73
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я думаю, то, что вы ищете, похоже на перегрузку функций. Насколько мне известно, Rust этого не поддерживает (как и в C). Однако систему типов Rust можно заставить работать на нас (не требуя чего-то вроде if constexpr или SFINAE в C++) следующим образом:

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();
}
Ответ принят как подходящий

Итак, основываясь на вашем MRE, у меня есть два варианта.

  1. Два трейта: DataSet и CacheableDataSet, в каждом одинаковые методы, кроме execute и 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
    }
}

детская площадка

  1. Три трейта: DataSet со всеми общими методами, DataSetX для execute и CacheableDataSetX для 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
    }
}

детская площадка

Лично я предпочитаю № 1, потому что для каждого типа DataSet нужно реализовать только одну черту.

Большое спасибо @PitaJ! Это то, что я искал. Я, вероятно, последую вашему совету и возьму № 1.

Anatoly Bugakov 18.02.2023 21:43

Другие вопросы по теме