In the context of building a graph, I have these Read traits, one for nodes and one for edges:
// node::Read
// for nodes that I can read
pub trait Read {
type Unit;
fn read(&self) -> &Self::Unit;
}
// edge::Read
// for edges that I can read
pub trait Read {
type Unit;
fn read<F: FnOnce(&Self::Unit)>(&self, read: F);
}
I have some node types where I want to implement the first Read so I can read the Unit and just clone the result from client or whatever:
pub struct MyNode<U> {
unit: U,
reactors: Reactors,
}
impl<U> Read for MyNode<U> {
type Unit = U;
fn read(&self) -> &Self::Unit {
&self.unit
}
}
But I also have some edge types where I want to implement the other Read so I am required to provide a closure to read the Unit:
pub struct MyEdge<S> {
pub root: Option<Reactor>,
pub stem: Arc<RwLock<S>>,
pub meta: Meta,
}
impl<S> edge::Read for MyEdge<S>
where
S: node::Read,
{
type Unit = S::Unit;
fn read<F: FnOnce(&Self::Unit)>(&self, read: F) {
let stem = self.stem.read().expect(NO_POISON);
read(&stem.read());
}
}
Right now, I put the two different reads under separate modules for name spacing. But I am questioning this decision because now I want to implement edge::Read for things that are not really edges (but could be placed where an edge would normally go) and I want to implement node::Read for things that aren't really nodes either. Should I reorganize traits like this so it makes sense to use them in broader context? edge::Read could be ReadInner and live next to Read? Or is this a sign that my traits would lose meaning? I could write traits that are exactly the same but in a different module for the things that are not node or edge like. I am looking for guidelines/ideas on this. Thank you.
It seems to me that the problem is that the two different traits are simultaneously serving two purposes to you: distinguishing between nodes and edges, and distinguishing between two different levels of ability to provide a value (by reference or only in a callback).
The first question to ask yourself is: Are you benefiting from having these traits at all? That is, are those traits used in any generic code as <T: Read>? If not, then delete them and write inherent methods instead.
If they are needed, then I would suggest discarding the idea that they are about nodes and edges, and just define them as two kinds of reading. If you need "this is an edge" as a trait bound, make that a separate trait.
Especially when you're running into categorization issues like this, the general advise is to make traits as narrow in functionality as they can be. Rather than "node that can be read", "element that can lend out its unit value". Rather than "edge that can be read", "element that can lend its unit value to a callback".
These descriptors sound less generic, but because they're defined in terms of constrained functionality, they're actually more general as more types can impl the functionality than can be the prior trait identity.
Also potentially illuminating is that you could provide:
impl<T: ?Sized> edge::Read for T
where
T: node::Read,
{
type Unit = <Self as node::Read>::Unit;
fn read<F: FnOnce(&Self::Unit)>(&self, read: F) {
let unit = node::Read::read(self);
read(unit);
}
}
edge::Read is exposing an "internal" operation and node::Read an "external" operation. Here's one explanation of the conceptual difference. "Internal" is more general for the implementor, and "external" is more general for the consumer.
If that's the logical split you go with, I'd personally name them Read and ReadWith, as "with" seems to match the usual convention for this kind of closure (see e.g. LocalKey::with).
Does this bit count as <T: Read>? I believe the traits are needed so I can have different concrete edges and nodes. For example, I could give a leaf node or a computational node to an edge and Read is just one requirement imposed by the edge. So sticking with the idea that I need them, I agree that I should distinguish them based on what kind of reading they are doing. Thanks for your input.
If you need "this is an edge" as a trait bound, make that a separate trait.
I imagine that could be what they call a marker trait if it doesn't define any methods?
Okay it appears that Read and ReadWith should be the way to go. That code snippet is helpful and greatly appreciated because it might help with some other things I was trying to work out as well.
Rather than "node that can be read", "element that can lend out its unit value". Rather than "edge that can be read", "element that can lend its unit value to a callback".