Working around absence of associated type constructor


#1

Hi everyone,

While playing around with Rust, I think I just ended up on a nice use case for associated type constructors, which currently do not exist (although there’s a proposal to add them). So I’m looking for the nicest workaround that today’s Rust will give me, if anyone feels like helping.

Here is the design which I am trying to express:

  • Let there be two actors A and B, with a communication channel in between
  • The communication channel can take many forms: shared memory, network connection, message-passing library… My goal in this project is to abstract this diversity away.
  • For the use cases which I have in mind, the proper abstraction is distributed shared memory. I want to give A and B the illusion that they share memory (with relaxed consistency semantics) even if they don’t.
  • I want the abstraction to have as low a runtime overhead as possible. If A and B know in advance how they want to communicate, the compiler should generate optimal code for that communication channel.

From a code point of view, my current interface design is somewhat similar to this:

// Shared data block abstraction, somewhat inspired by RefCell
trait SharedData<T> {
    // Get read access to the latest available copy of the shared data.
    // The access is guaranteed by the implementation to be data-race-free.
    type DataRef: Deref<Target=T> + Drop;
    fn borrow(&self) -> DataRef;

    // Get write access to the shared data. Changes will be atomically
    // propagated to clients once the reference wrapper gets out of scope.
    type DataRefMut: DerefMut<Target=T> + Drop;
    fn borrow_mut(&mut self) -> DataRefMut;
}

// A communication channel between A and B. Can be unsynchronized shared
// memory (for coroutines), synchronized shared memory (for threads),
// UNIX sockets or MPI (for distributed processes)... That's abstracted away!
trait CommunicationChannel {
    // Shared data block implementation that is optimal for the underlying channel
    type SharedDataImpl<T>: SharedData<T>;  // THIS WON'T COMPILE

    // Create a new shared data block with some initial value
    fn new_shared_data<T>(&mut self, init: T) -> SharedDataImpl<T>;
}

This is a highly simplified version of the interface which I actually have in mind. In particular, it lacks…

  • Several important generic bounds needed for implementation
  • Better synchronization mechanisms than polling
  • A mechanism to forbid concurrent writes

But the minimized code is enough to show the problem, which is that I cannot declare the SharedDataImpl associated type constructor in today’s Rust.

This interface could be made legal by an appropriate version of the associated type constructor RFC proposal ( https://github.com/rust-lang/rfcs/pull/1598 ). For now, people on this Github issue have mostly focused on genericity over lifetimes, whereas I want to be generic over types. Perhaps I should submit some feedback on why I think that genericity over types is also useful. Or not: looking at the RFC, the author seems to already have this in mind.

In meantime, I have thought about two workarounds:

  • Use the new impl SharedData<T> syntax. I don’t think this is allowed on a trait function. In addition, this is a nightly feature, and it does not quite match the semantics that I want to express in my trait (where the function return type will always be the same for any given T).
  • Give up on full static polymorphism for now, and return a Box<SharedData<T>>. Will require some unnecessary dynamic memory allocation and a vtable, and has the same issue as above, but at least I think it would work on stable.

Can someone more knowledgeable about Rust than me think about a smarter way to do this that I have overlooked?


#2

I’m probably missing something, but would the following work for you:

use std::ops::{Deref, DerefMut};
// Shared data block abstraction, somewhat inspired by RefCell
trait SharedData {
    type Output;
    // Get read access to the latest available copy of the shared data.
    // The access is guaranteed by the implementation to be data-race-free.
    type DataRef: Deref<Target=Self::Output> + Drop;
    fn borrow(&self) -> Self::DataRef;

    // Get write access to the shared data. Changes will be atomically
    // propagated to clients once the reference wrapper gets out of scope.
    type DataRefMut: DerefMut<Target=Self::Output> + Drop;
    fn borrow_mut(&mut self) -> Self::DataRefMut;
}

// A communication channel between A and B. Can be unsynchronized shared
// memory (for coroutines), synchronized shared memory (for threads),
// UNIX sockets or MPI (for distributed processes)... That's abstracted away!
trait CommunicationChannel {
    // Shared data block implementation that is optimal for the underlying channel
    type SharedData: SharedData;

    // Create a new shared data block with some initial value
    fn new_shared_data<T>(&mut self, init: T) -> Self::SharedData;
}

#3

Unfortunately, I don’t think so. As far as I understand Rust, in your proposal, CommunicationChannel::SharedData may only designate one type of shared data (say, a SharedData<Output=i32> or a SharedData<Output=String>), and not the generic concept of shared data.

As a result, the new_shared_data() generic factory could now only return one type of shared data, and not shared data of any type, i.e. it would not be a generic factory anymore.

Maybe I could make this sort of design work by using SharedData<Core::Any>, though.