I'm new to Rust and I'm building an Adapter struct that has a cache to store fetched data locally. I had it working successfully for a HashMap but wanted to generalize it to let users swap this out for any other type that implements my Cache Trait.
How can I refactor this to an interface-like implementation?
pub trait Cache {
fn insert(&mut self, data: Budgets) -> anyhow::Result<()>;
}
// #[derive(Debug)]
pub struct Adapter<'a, T: Cache> {
base_uri: &'a str,
client: reqwest::blocking::Client,
default_budget: Option<&'a str>,
//cache: HashMap<String, Budget> <-- original impl
//cache: &'a dyn Cache <-- Not sure how to make this work
cache: T
}
I was able to get a hacky solution working where I used the above uncommented code and then created a new struct for my trait:
pub struct HashCache {
cache: HashMap<String, Budget>
}
impl Cache for HashCache {
fn insert(&mut self, data: Budgets) -> anyhow::Result<()> {
for budget in data.budgets.into_iter() {
self.cache.insert(budget.name.clone(), budget);
}
Ok(())
}
}
This then required me to make a dirty new method requiring a concrete type parameter:
impl<'a> Adapter<'a, HashCache> {
pub fn new(bearer: String) -> Self {
// -- other fields --
//cache: HashMap::new()
cache: HashCache { cache: HashMap::new() }
}
}
And now that I have to go through a struct, traversing the cache has an extra issue:
pub fn show_accounts(&self, budget_name: &str) {
if let Some(b) = self.cache.cache.get(budget_name) { <-- Requires getting into the struct
if let Some(a) = &b.accounts {
println!("{:?}", a)
} else {
println!{"No accounts present"}
}
}
}
Since you define Cache in your crate, it should be possible to directly implement the trait on HashMap:
impl Cache for HashMap<String, Budget> {
// ...
}
To avoid the "dirty new method" problem, you could also require that the type implement Default, so you could write e.g.
impl<'a, T: Cache + Default> Adapter<'a, T> {
pub fn new(bearer: String) -> Self {
// -- other fields --
cache: T::default()
}
}
}
In the case of show_accounts(), since you're using a generic T, you shouldn't hardcode HashMap methods. Instead, add those methods to the Cache trait:
pub trait Cache {
fn get(s: String) -> Option<Budget>;
fn insert(&mut self, data: Budgets) -> anyhow::Result<()>;
// -- other methods --
}
Related
Given a situation where we receive inputs for some nodes type like 'nodeA' or 'nodeB', and we want to initialize structs with that same input. Is it possible without a gigantic switch block? These structs share similar behaviour (using a Trait for that) but with some differences.
pub trait Executable {
fn run(&self);
}
pub struct NodeA {}
impl Executable for NodeA {
fn run(&self) {}
}
pub struct NodeB {}
impl Executable for NodeB {
fn run(&self) {}
}
Flow:
User inputs 'nodeA'
Program initializes struct nodeA with some data
User inputs 'nodeB'
Program initializes struct nodeB with some data
...
To specify better, the final use case is reading a JSON file with all the nodes and respective params to be instantiated. Some of those nodes can come from external plugins, so the number of existing nodes can become very big.
For smaller, static number of nodes, I think a match - case construct is perfectly fine.
But if you have a larger number of nodes, or the available nodes is dynamically changing, I would implement something like this:
pub trait Executable {
fn run(&self);
}
pub struct NodeA {}
impl Executable for NodeA {
fn run(&self) {
println!("NodeA::run()");
}
}
pub struct NodeB {}
impl Executable for NodeB {
fn run(&self) {
println!("NodeB::run()");
}
}
pub trait Matcher {
fn try_match(&self, s: &str) -> Option<Box<dyn Executable>>;
}
pub struct NodeAMatcher;
pub struct NodeBMatcher;
impl Matcher for NodeAMatcher {
fn try_match(&self, s: &str) -> Option<Box<dyn Executable>> {
(s == "NodeA").then(|| Box::new(NodeA {}) as Box<dyn Executable>)
}
}
impl Matcher for NodeBMatcher {
fn try_match(&self, s: &str) -> Option<Box<dyn Executable>> {
(s == "NodeB").then(|| Box::new(NodeB {}) as Box<dyn Executable>)
}
}
struct MatcherRegistry {
matchers: Vec<Box<dyn Matcher>>,
}
impl MatcherRegistry {
fn new() -> Self {
Self { matchers: vec![] }
}
fn register_matcher(&mut self, matcher: impl Matcher + 'static) {
self.matchers.push(Box::new(matcher));
}
fn try_get_node(&self, s: &str) -> Option<Box<dyn Executable>> {
self.matchers
.iter()
.filter_map(|matcher| matcher.try_match(s))
.next()
}
fn try_execute(&self, s: &str) {
if let Some(node) = self.try_get_node(s) {
node.run();
} else {
println!("'{}' not found.", s);
}
}
}
fn main() {
let mut registry = MatcherRegistry::new();
registry.register_matcher(NodeAMatcher);
registry.register_matcher(NodeBMatcher);
registry.try_execute("NodeA");
registry.try_execute("NodeB");
registry.try_execute("NodeC");
}
NodeA::run()
NodeB::run()
'NodeC' not found.
Here, you have a factory pattern.
The structs NodeAMatcher and NodeBMatcher are factories for NodeA and NodeB. They can check if the input matches, and then create an Executable object.
Then, you collect all possible factories (or Matchers here) in a registry, here called MatcherRegistry. You can then, at runtime, add or remove matchers as you wish.
Of course, if you don't need to create a new object every time and the act of executing doesn't consume it, you can reduce the complexity a little by bypassing the factory pattern:
use std::collections::HashMap;
pub trait Executable {
fn run(&self);
}
pub struct NodeA {}
impl Executable for NodeA {
fn run(&self) {
println!("NodeA::run()");
}
}
pub struct NodeB {}
impl Executable for NodeB {
fn run(&self) {
println!("NodeB::run()");
}
}
struct ExecutableRegistry {
executables: HashMap<&'static str, Box<dyn Executable>>,
}
impl ExecutableRegistry {
fn new() -> Self {
Self {
executables: HashMap::new(),
}
}
fn register_executable(
&mut self,
command: &'static str,
executable: impl Executable + 'static,
) {
self.executables.insert(command, Box::new(executable));
}
fn try_execute(&self, s: &str) {
if let Some(node) = self.executables.get(s) {
node.run();
} else {
println!("'{}' not found.", s);
}
}
}
fn main() {
let mut registry = ExecutableRegistry::new();
registry.register_executable("NodeA", NodeA {});
registry.register_executable("NodeB", NodeB {});
registry.try_execute("NodeA");
registry.try_execute("NodeB");
registry.try_execute("NodeC");
}
Of course there exists a large mount of other variations of the same patterns. Which one you implement is up to you and your usecase.
I have a configuration struct that looks like this:
struct Conf {
list: Vec<String>,
}
The implementation was internally populating the list member, but now I have decided that I want to delegate that task to another object. So I have:
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf<T: Sized + ListBuilder> {
list: Vec<String>,
builder: T,
}
impl<T> Conf<T>
where
T: Sized + ListBuilder,
{
fn init(&mut self) {
self.builder.build(&mut self.list);
}
}
impl<T> Conf<T>
where
T: Sized + ListBuilder,
{
pub fn new(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: lb,
};
c.init();
c
}
}
That seems to work fine, but now everywhere that I use Conf, I have to change it:
fn do_something(c: &Conf) {
// ...
}
becomes
fn do_something<T>(c: &Conf<T>)
where
T: ListBuilder,
{
// ...
}
Since I have many such functions, this conversion is painful, especially since most usages of the Conf class don't care about the ListBuilder - it's an implementation detail. I'm concerned that if I add another generic type to Conf, now I have to go back and add another generic parameter everywhere. Is there any way to avoid this?
I know that I could use a closure instead for the list builder, but I have the added constraint that my Conf struct needs to be Clone, and the actual builder implementation is more complex and has several functions and some state in the builder, which makes a closure approach unwieldy.
While generic types can seem to "infect" the rest of your code, that's exactly why they are beneficial! The compiler knowledge about how big and specifically what type is used allow it to make better optimization decisions.
That being said, it can be annoying! If you have a small number of types that implement your trait, you can also construct an enum of those types and delegate to the child implementations:
enum MyBuilders {
User(FromUser),
File(FromFile),
}
impl ListBuilder for MyBuilders {
fn build(&self, list: &mut Vec<String>) {
use MyBuilders::*;
match self {
User(u) => u.build(list),
File(f) => f.build(list),
}
}
}
// Support code
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct FromUser;
impl ListBuilder for FromUser {
fn build(&self, list: &mut Vec<String>) {}
}
struct FromFile;
impl ListBuilder for FromFile {
fn build(&self, list: &mut Vec<String>) {}
}
Now the concrete type would be Conf<MyBuilders>, which you can use a type alias to hide.
I've used this to good effect when I wanted to be able to inject test implementations into code during testing, but had a fixed set of implementations that were used in the production code.
The enum_dispatch crate helps construct this pattern.
You can use the trait object Box<dyn ListBuilder> to hide the type of the builder. Some of the consequences are dynamic dispatch (calls to the build method will go through a virtual function table), additional memory allocation (boxed trait object), and some restrictions on the trait ListBuilder.
trait ListBuilder {
fn build(&self, list: &mut Vec<String>);
}
struct Conf {
list: Vec<String>,
builder: Box<dyn ListBuilder>,
}
impl Conf {
fn init(&mut self) {
self.builder.build(&mut self.list);
}
}
impl Conf {
pub fn new<T: ListBuilder + 'static>(lb: T) -> Self {
let mut c = Conf {
list: vec![],
builder: Box::new(lb),
};
c.init();
c
}
}
I'd like to use Factory to build an object from the String and have multiple impls: 1) actual building and 2) caching (stores in-memory in HashMap). The problem is that in case #1 it have to pass the ownership and in case #2 HashMap owns the value and a reference can be returned only.
use std::collections::HashMap;
// product interface
pub trait TProduct {
fn get_title(&self) -> &String;
}
// and concrete impls
pub struct ConcreteProduct1 {
}
impl TProduct for ConcreteProduct1 {
// ...
}
pub struct ConcreteProduct2 {
}
impl TProduct for ConcreteProduct2 {
// ...
}
// factory interface
pub trait TProductFactory {
fn product_from_text(&mut self, text: String) -> Box<dyn TProduct>;
// QUESTION: should it be Box (required for ProductFactory) or &Box (required for ProductCachingProxy)?
}
// actual building factory
pub struct ProductFactory {
}
impl TProductFactory for ProductFactory {
fn product_from_text(&mut self, text: String) -> Box<dyn TProduct> {
//...
// depending on some conditions
Box::new(ConcreteProduct1::from_text(text)); // has to pass the ownership
// or
Box::new(ConcreteProduct2::from_text(text)); // has to pass the ownership
//...
}
}
// caching proxy
trait TProductCache {
fn put(&mut self, text: &String, product: Box<dyn TProduct>);
fn get(&self, text: &String) -> Option<&Box<dyn TProduct>>;
fn clear(&mut self);
}
struct InMemoryProductCache {
map: HashMap<String, Box<dyn TProduct>>
}
impl InMemoryProductCache {
fn new() -> Self {
return InMemoryProductCache {
map: HashMap::new()
}
}
}
impl TProductCache for InMemoryProductCache {
fn put(&mut self, text: &String, product: Box<dyn TProduct>) {
self.map.insert(text.to_string(), product);
}
fn get(&self, text: &String) -> Option<&Box<dyn TProduct>> {
return match self.map.get(text) {
Some(boxed_product) => Some(boxed_product), // have to pass a reference to let it still own the value
None => None
}
}
fn clear(&mut self) {
self.map.clear();
}
}
struct ProductCachingProxy {
product_factory: Box<dyn TProductFactory>,
cache: Box<dyn TProductCache>
}
impl ProductCachingProxy {
fn new_for_factory(product_factory: Box<dyn TProductFactory>, cache: Box<dyn TProductCache>) -> Self {
return ProductCachingProxy {
product_factory,
cache
}
}
}
impl TProductFactory for ProductCachingProxy {
fn product_from_text(&mut self, text: String) -> &Box<dyn TProduct> { // can't pass ownership
let boxed_product = match self.cache.get(&text) {
Some(found_boxed_product) => found_boxed_product,
_ => {
// delegate creation to wrapped TProductFactory impl (`product_factory`)
let boxed_product = self.product_factory.product_from_text(text.clone());
// ... and put to the cache
self.cache.put(&text, boxed_product);
&boxed_product
}
};
return boxed_product;
}
}
// QUESTION: should it be Box (required for ProductFactory) or &Box (required for ProductCachingProxy) to be returned from TProductFactory.fn product_from_text(&mut self, text: String) -> Box<dyn TProduct>; ?
If caching proxy to return a Box, how can it be created from a reference without copying/cloning (TProductCache.get(..))?
Replace Box with Rc (or Arc if you use threads). It provides shared ownership and suites both your cases with single signature. Another option is to use Cow that is a enum of owned and borrowed states.
This question is based on information given in another recent question of mine. I would like to extend the following Container with a remove method that returns ownership of the stored data to the caller. The accompanying unit test should explain its desired behaviour.
In the previous case (see referenced question), I would have used the downcast method on the Box<Any> object, but I don't know how to approach the problem in the case where enums are used instead. I'm grateful for pointers.
use std::any::{Any, TypeId};
use std::collections::HashMap;
trait GroupTrait {
fn borrow<T: Any>(&self) -> Option<&T>;
}
struct Container<G> {
inner: HashMap<TypeId, G>,
}
impl<G> Default for Container<G>
where
G: GroupTrait,
{
fn default() -> Self {
Container {
inner: Default::default(),
}
}
}
impl<G> Container<G>
where
G: GroupTrait,
{
pub fn insert<T: Any + Into<G>>(&mut self, data: T) {
self.inner.insert(TypeId::of::<T>(), data.into());
}
pub fn borrow<T: Any>(&self) -> Option<&T> {
self.inner.get(&TypeId::of::<T>()).and_then(|g| g.borrow())
}
pub fn remove<T: Any>(&mut self) -> Option<T> {
unimplemented!()
}
}
#[cfg(test)]
mod tests {
use super::*;
/// This should be an user-defined type that implements the Any trait.
#[derive(Debug, Clone, PartialEq)]
struct TypeA(u32);
/// This should be an user-defined type that implements the Any trait.
#[derive(Debug, Clone, PartialEq)]
struct TypeB(String);
/// This is the enum that should replace boxed `Any` trait objects. Users also need to supply
/// this enum. Maybe they'll need to implement additional traits to get `borrow` to work.
#[derive(Debug, PartialEq)]
enum Group {
A(TypeA),
B(TypeB),
}
impl From<TypeA> for Group {
fn from(value: TypeA) -> Self {
Group::A(value)
}
}
impl From<TypeB> for Group {
fn from(value: TypeB) -> Self {
Group::B(value)
}
}
impl GroupTrait for Group {
fn borrow<T: Any>(&self) -> Option<&T> {
use self::Group::*;
match *self {
A(ref i) => Any::downcast_ref(i),
B(ref i) => Any::downcast_ref(i),
}
}
}
#[test]
fn insert() {
let mut c: Container<Group> = Default::default();
let data = TypeA(100);
c.insert(data.clone());
assert_eq!(
c.inner.get(&TypeId::of::<TypeA>()),
Some(&Group::A(data.clone()))
);
}
#[test]
fn borrow() {
let mut c: Container<Group> = Default::default();
let data = TypeA(100);
c.insert(data.clone());
let borrowed = c.borrow::<TypeA>();
assert_eq!(borrowed, Some(&data));
}
#[test]
fn remove() {
let mut c: Container<Group> = Default::default();
let data = TypeA(100);
c.insert(data.clone());
assert_eq!(c.remove::<TypeA>(), Some(data));
}
}
As you mentioned in the comments, TryFrom is possible. However, I'd go with Into<Option<T>>:
pub fn remove<T: Any>(&mut self) -> Option<T>
where
G: Into<Option<T>>,
{
self.inner.remove(&TypeId::of::<T>()).and_then(|g| g.into())
}
Playground
I'd choose Into<Option<T>> over TryInto<T> because Into<Option<T>> results in an Option while TryInto<T> results in Result<T, Self::Error>
I'm having problems with a struct containing a hashmap in rust.
Lets say I have the following types:
pub type KeyType i32;
pub enum StatusType { Locked, Unlocked }
pub struct Entry {
key: KeyType,
status: StatusType
}
pub struct Manager<'a> {
map: HashMap<KeyType, &'a mut Entry>
}
I want to define a method on Manager that takes a key and returns either a unlocked entry if not found, or the existing entry if it exists. Here is the pseudo code:
impl<'a> Manager<'a> {
pub fn find_or_create_entry(&'a mut self, key: KeyType) -> &'a mut Entry {
match self.map.get(&key) {
Some(e) => e,
None => {
// create new entry, add to map, and return mutable ref
}
}
}
}
I haven't been able to figure out how this works in Rust. Any pointers?
I got around this by changing the type of the HashMap to HashMap<KeyType,Box<Entry>>, and implemented the method as follows:
pub fn get_or_create_entry(& mut self, key: LockKey) -> &mut LockEntry {
let e = Box::new(LockEntry{key: key, status: LockStatus::Unlocked});
self.lock_table.entry(key).or_insert(e)
}
Is there a better way?