Требование высокого уровня: я хочу предоставить пользователям моей библиотеки одну функцию. Что-то вроде:
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(¶m);
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
.
Наверняка я не первый, кто пытается этого добиться :) Есть ли лучший способ?
Большое спасибо @PitaJ, я изучу этот подход. Единственное, поскольку CacheableDataSet является «подмножеством» DataSet, я бы сделал что-то вроде trait CacheableDataSet: DataSet {...}
У вас могут возникнуть конфликты с impl<T: CacheableDataSet + ?Sized> DataSet for T
, если вы сделаете CacheableDataSet
суперчертой DataSet
.
В частности, если у вас есть какие-либо методы не по умолчанию для DataSet
, это будет невозможно. Таким образом, вам нужно будет либо переместить любые общие методы не по умолчанию в третью черту, либо НЕ делать ее суперчертой и дублировать все методы не по умолчанию.
@PitaJ, большое спасибо. Действительно, я столкнулся с проблемой других методов DataSet
, не реализованных для CacheableDataSet
. (их довольно много). Не могли бы вы подсказать мне, как это можно решить с помощью другого (третьего) признака?
@PitaJ, чтобы усложнить ситуацию, execute_cacheable
подпись на самом деле execute_cacheable<DS: DataSet + Cacheable + ?Sized>data: Arc<RwLock<DS>>
(я должен был сразу упомянуть об этом в вопросе). Arc, потому что он является общим для потоков и RwLock, чтобы разрешить чтение и запись в кеш.
Предоставьте минимальный воспроизводимый пример с полными чертами, типами и т. д., которые вы используете.
@PitaJ, мои извинения. Я думал, что это будет простой. Я исправил вопрос с помощью MRE того, чего я пытаюсь достичь.
Я думаю, то, что вы ищете, похоже на перегрузку функций. Насколько мне известно, 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, у меня есть два варианта.
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
}
}
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.
Что-то вроде этого, может быть? play.rust-lang.org/…