Hi, I am trying to implement a generic struct NodeValue that has the following properties:
- hold a generic value of type T
- provides different versions with different access methods
- for this simple example just one where the struct implements a send request function and one where it does not
- I want to store references to objects of this struct NodeValue in a common array where each referenced object may have a different generic value type T and provides different access functions
- for this, I don't want to use dynamic dispatch but have one enum for each value T variant instead.
- but each access type is treated the same way, thus the enum just differentiates between T types.
The purpose of this is to be able to define NodeValues with different access rights (for sending values over the network or receiving values), while being able to process a list of all defined NodeValues by simply writing match expressions over the NodeValue type.
The usage would look like this:
let node1: NodeValue<i32, NotWritable> = NodeValue::new(5);
// node.send_write_request(); // should cause a compile error
let node2_writable: NodeValue<f32, Writable> = NodeValue::new(0.1);
node2_writable.send_write_request(); // is ok
// now we can store the references in a common array
let node_refs_enum: Vec<NodeValueRef> = vec![
NodeValueRef::I32(&node1),
NodeValueRef::F32(&node2_writable)
];
// do operations of nodes by matching on their type
for node in node_refs_enum {
match node {
NodeValueRef::F32(node_ref) => println!("got F32: {}", node_ref.value),
NodeValueRef::I32(node_ref) => println!("got I32: {}", node_ref.value),
}
}
I nearly could implement all as I wanted, but I needed to handle each "access-type" by a separate enum. I would like to get rid of this. Does someone know a better/cleaner way of implementing this so I can use the NodeValue struct as described above?
Here is the kind of working version:
pub trait NodeValueWritable {
fn send_write_request(&self);
}
// Dummy structs
struct NotWritable {}
struct Writable {}
impl NodeValueWritable for Writable {
fn send_write_request(&self) {}
}
#[derive(Debug)]
pub struct NodeValue<T, ACCESS> {
pub value: T,
// other attributes ...
access_type: PhantomData<ACCESS>
}
impl<T, ACCESS> NodeValue<T, ACCESS> {
pub fn new(value: T) -> NodeValue<T, ACCESS> {
return NodeValue{
value: value,
access_type: PhantomData
}
}
}
impl<T, ACCESS> NodeValueWritable for NodeValue<T, ACCESS> where ACCESS: NodeValueWritable {
fn send_write_request(&self) {
// ...
}
}
/**
* Wraps NodeValue references with access type parameter.
* Would like to avoid this!!
*/
pub enum NodeValueRefWithAccess<'a, T> {
NotWritable(&'a NodeValue<T, NotWritable>),
Writable(&'a NodeValue<T, Writable>),
}
/**
* Wraps NodeValue references into an enum to avoid using dynamic dispatch.
*/
pub enum NodeValueRef<'a> {
F32(NodeValueRefWithAccess<'a, f32>),
I32(NodeValueRefWithAccess<'a, i32>),
}
For defining and going over a list of NodeValues I always need an inner match expression over the access type, which is totally unnecessary since NodeValue always has the same data members no matter the access type (I guess this also adds some runtime overhead):
// now we can store the references in a common array
let node_refs_enum: Vec<NodeValueRef> = vec![
NodeValueRef::I32(NodeValueRefWithAccess::NotWritable(&node1)),
NodeValueRef::F32(NodeValueRefWithAccess::Writable(&node2_writable)),
// would be nicer to not have to use the NodeValueRefWithAccess enum
];
// do operations of nodes by matching on their type
for node in node_refs_enum {
match node {
NodeValueRef::F32(node_ref) => match node_ref {
// want to avoid this inner match expression
NodeValueRefWithAccess::NotWritable(node) => println!("got F32: {}", node.value),
NodeValueRefWithAccess::Writable(node) => println!("got F32: {}", node.value),
},
NodeValueRef::I32(node_ref) => match node_ref {
NodeValueRefWithAccess::NotWritable(node) => println!("got I32: {}", node.value),
NodeValueRefWithAccess::Writable(node) => println!("got I32: {}", node.value),
},
}
}