Previously I had almost identical storage implemented for my components & nodes, so I thought I'd merge them into one storage type that I can wrap.
Only difference is with nodes, my values are an associated type, not the same type as is used for the typeid (TypeId<NodeRef> => NodeRef::RecipeTuple) while my component storage is just TypeId<Component> => Component.
I thought I could use specialization to implement a trait specifying the value type corresponding to another type, but that has been giving me a lot of trouble, so I thought I'd ask here what the best approach is?
#How/can I unify my two storage types
32 messages · Page 1 of 1 (latest)
I thought I could use specialization
Bad idea.
Specialization honestly barely works, not to mention the fact it's unsound in extremely subtle ways
Now for the question
You posted a big ball of code, but I don't actually understand what you need help with. Does that code not work? Does it do something you wish it didn't? If so, what?
yeah none of the T::Value stuff I have actually works right now. No approach I have taken to try defining and implementing MiniTypeMapKey has both avoided overlap and resulted in a T::Value that the compiler could properly infer based on T. So I'm not sure if there's a way to get MiniTypeMapKey working or if I should just have MiniTypeMap's functions require a separate Key & Value type. If I go with totally separate Key & Value types it should work, but the downside would be that nearly all the functions would be unsafe since there's no guarantee the pair Key & Value types being used to retrieve is the same as was used to insert.
Bear with me for a minute, sorry.
You are trying to define a MiniTypeMapKey trait, such that, given the type of the key, you can get the type of the value. So far so reasonable.
What I am a bit confused by is TargetValue. What's that trait for? It appears you're using it to write an impl that says that if T: TargetValue, then T is its own key, which is... confusing to see. What would that impl be used for?
this wouldn't compile because the second impl isn't actually more specific:
pub trait MiniTypeMapKey {
type Value: Send + Sync;
}
default impl<T: Send + Sync /* T must implement these for Value */> MiniTypeMapKey for T {
type Value = T;
}
impl<T: NodeRef /* Doesn't implement Send + Sync */> MiniTypeMapKey {
type Value = T::RecipeTuple;
}
so I had created a separate trait so Value doesn't need Send + Sync (meaning T doesn't need to be Send + Sync), then I can separately insure Value is Send + Sync here:
impl<T: TargetValue + 'static> MiniTypeMapKey for T
where
T::Target: Send + Sync,
{
type Value = T::Target;
}
this one did actually compile without overlapping impls but the compiler couldn't infer Value and I got it to crash a lot 😅
so from this...
pub trait MiniTypeMapKey {
type Value: Send + Sync;
}
default impl<T: Send + Sync /* T must implement these for Value */> MiniTypeMapKey for T {
type Value = T;
}
impl<T: NodeRef /* Doesn't implement Send + Sync */> MiniTypeMapKey {
type Value = T::RecipeTuple;
}
to this...
trait TargetValue {
type Target;
}
default impl<T> TargetValue for T {
type Target = T; // Value doesn't need Send + Sync, so T doesn't need it either.
}
// I think I was missing this in what I sent above:
impl<T: NodeRef> TargetValue for T {
type Target = T::RecipeTuple; // This implements Send + Sync.
}
pub trait MiniTypeMapKey {
type Value: Send + Sync;
}
// This should only be implemented for all NodeRefs (since RecipeTuple is always Send + Sync)
// and for and other type T that is, itself Send + Sync
impl<T: TargetValue> MiniTypeMapKey for T where T::Target: Send + Sync {
type Value = T::Target;
}
Oh, that's what the specialization is for.
A solution that often works is what I like to call "inference specialization", in which you don't actually use specialization (which, remember, is extremely broken and unsound) and instead rely on inference to more-or-less-hide an additional generic parameter:
pub trait MiniTypeMapKey<Disambiguator> {
type Value: Send + Sync;
}
struct OwnValue;
impl<T: Send + Sync> MiniTypeMapKey<OwnValue> for T {
type Value = T;
}
struct RecipeTuple;
impl<T: NodeRef> MiniTypeMapKey<RecipeTuple> {
type Value = T::RecipeTuple;
}
```After you do this, functions that want to take a `K: MiniTypeMapKey` need to change too:
```rs
fn example<D, K: MiniTypeMapKey<D>>(key: K)
```This gets you most of the way there. The trick is that, given a type `Foo` that only meets the conditions for one of the two impls (in this case, this means it needs to be `Send + Sync` XOR `NodeRef`), inference can figure out what type `D` is and `example(some_foo)` just works.
The catch is that I expect most of your `NodeRef`s happen to be `Send + Sync`, so you end up having to specify the `D` generic at callsites anyway. There's not _much_ you can do there. You could add an additional trait bound to the `OwnValue` impl, using a trait that you implement on most types (remember it can't be a blanket) but not on `NodeRef`, I guess.
The catch is that I expect most of your NodeRefs happen to be Send + Sync, so you end up having to specify the D generic at callsites anyway. There's not much you can do there.
Actually none of the NodeRefs are Send + Sync, only their associated type is.
You could add an additional trait bound to the OwnValue impl, using a trait that you implement on most types (remember it can't be a blanket) but not on NodeRef, I guess.
Would an auto trait with a negative trait impl for NodeRef do?
It would, though auto traits are another feature that is never meant to become stable
It is, however, not broken
Actually none of the NodeRefs are Send + Sync,
Well, that should solve the problem with no auto traits
Even though Send + Sync could technically get implemented, it is fine as long as it isn't implemented?
I'm implementing it now and I'll see how it goes :)
As long as it isn't implemented, inference will pick up the slack and let example(foo) just work
If it is implemented, you end up having to specify: example::<RecipleTuple, _>(foo)
Seems it does want me to specify the other generic parameter here for some reason?
(this function is on my ComponentStorage which is just a tuple struct wrapping MiniTypeMap):
pub fn get_components<T: 'static + Send + Sync>(
&'a mut self,
) -> impl ExactSizeIterator<Item = &'a mut T> {
self.0.values_mut::<T>().map(|cell| cell.get_mut())
}
Well, inference needs to have something to go off of
Oh, nevermind, misunderstood the question
Use _
If you specify any generic, you have to specify all of them, and you can use _ to leave them "explicitly unspecified"
it seems it can't infer it with that
108 | self.0.values_mut::<T, _>().map(|cell| cell.get_mut())
| ^^^^ ---- type must be known at this point
|
help: consider giving this closure parameter an explicit type, where the placeholders `_` are specified
|
108 | self.0.values_mut::<T, _>().map(|cell: &mut _| cell.get_mut())
| ++++++++
What's the current signature of values_mut?
pub fn values_mut<D, T: MiniTypeMapKey<D>>(&mut self) -> impl ExactSizeIterator<Item = &mut T::Value>
Wrong argument order, then. You need to specify T and let D be inferred, so that would be values_mut::<_, T>()
Not sure if it works, inference around closures gets finicky
seems to work, I'll finish doing that tomorrow since it's pretty late for me and I'll report back
If a user were to implement Send + Sync on a type implementing NodeRef, could they technically use either impl?
can confirm everything works now :)
only thing is I think there's one case of UB. The user could implement Send + Sync on a node and this would allow for inserting a different type of value (the NodeRef) than you could try getting back NodeRef::RecipeTuple. It is technically already UB to implement Send + Sync on a NodeRef though (unless it is a unit struct) but if it is a unit stuct, the RecipeTuple would also be a unit, so the downcast would technically still be correct.