#How/can I unify my two storage types

32 messages · Page 1 of 1 (latest)

severe arch
#

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?

glacial marsh
#

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

glacial marsh
severe arch
# glacial marsh You posted a big ball of code, but I don't actually understand what you need hel...

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.

glacial marsh
#

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?

severe arch
# glacial marsh Bear with me for a minute, sorry. You are trying to define a `MiniTypeMapKey` t...

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;
}

glacial marsh
# severe arch this wouldn't compile because the second impl isn't actually more specific: ```r...

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.
severe arch
# glacial marsh Oh, that's what the specialization is for. A solution that often works is what ...

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?

glacial marsh
#

It is, however, not broken

#

Actually none of the NodeRefs are Send + Sync,
Well, that should solve the problem with no auto traits

severe arch
#

I'm implementing it now and I'll see how it goes :)

glacial marsh
#

If it is implemented, you end up having to specify: example::<RecipleTuple, _>(foo)

severe arch
glacial marsh
#

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"

severe arch
#

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())
    |                                              ++++++++
glacial marsh
#

What's the current signature of values_mut?

severe arch
glacial marsh
#

Not sure if it works, inference around closures gets finicky

severe arch
severe arch
#

If a user were to implement Send + Sync on a type implementing NodeRef, could they technically use either impl?

severe arch
# glacial marsh Wrong argument order, then. You need to specify `T` and let `D` be inferred, so ...

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.