Working around specialisation


#1

I’m trying to work around the lack of specialisation in Rust, which would make solving this problem trivial. Take this contrived example:

trait Car {}

struct Electric;
impl Car for Electric {}

struct Petrol;
impl Car for Petrol {}

trait Accelerate<C: Car> {
    fn go(&self) where Self: Sized;
}

struct Fast<C> {
    _car: C
}

impl Accelerate<Electric> for Fast<Electric> {
    fn go(&self) {
        println!("Increase current loads");
    }
}

impl Accelerate<Petrol> for Fast<Petrol> {
    fn go(&self) {
        println!("Burn tonnes of fuel");
    }
}

This code compiles and works fine. However in the real world there are more Accelerate types than just Fast, and each type only works for a certain model of car, which is discovered at runtime. So, I’d like to use a factory pattern to encapsulate choosing the right Accelerate:

trait AccelerateFactory<C: Car> {
    fn factory(self) -> Box<Accelerate<C>>;
}

impl AccelerateFactory<Electric> for Electric {
    fn factory(self) -> Box<Accelerate<Electric>> {
       Box::new(Fast {
           _car: self
       })
   }
}

impl AccelerateFactory<Petrol> for Petrol {
    fn factory(self) -> Box<Accelerate<Petrol>> {
        Box::new(Fast {
            _car: self
        })
   }
}

This is where we get into trouble. If I try and run this code, I get the following:

let car = Petrol;
let accelerate = car.factory();
accelerate.go()

error: the go method cannot be invoked on a trait object

Even if I can find a way to appease the compiler, it’s going to get very verbose when we scale the different Car attributes (other than Accelerate), and their associated types (other than Fast). I’m struggling to come up with a design pattern than appeases the compiler and doesn’t result in tonnes of boilerplate like above.

Any help would be much appreciated!

Pete.


#2

It’s not clear why you put the Self: Sized restriction on the go method - it’s not needed in your example, and removing it would make this callable.

However, I’m having a hard time figuring out exactly what you’re trying to model. If I didn’t know any better, the contrived example looks a bit like the stereotypical Java-esque designs with lots of delegation, factories, etc :slight_smile:. For instance, why doesn’t the Car trait have more methods, such as go right in it? What else besides Fast are you going to have?


#3

Doh! Self: Sized was a hangover from my code, which I need because I’m returning a Box<Future<Self>>.

RE: Java-esque design - true. OO is my background. I’m still trying to get my head around Rust.

What I’m trying to accomplish is a bunch of OS abstractions that can be run either locally or remotely via a socket. Therefore Car = Host, Accelerate = Package or Service etc. and Fast = Freebsd or Yum or whatever type is specific to the target platform. To answer your question - putting more methods on Car (i.e. Host) would make it difficult to abstract target-specific logic. This might be a difficult question to answer with such little context, but could you recommend a more Rusty design pattern?

Thanks mate :):slightly_smiling_face:


#4

I’m afraid it is a bit difficult to answer without more context. But I’m glad we’ve at least established it’s not really about cars :slight_smile:.

Can you sketch out some pseudocode using the real concepts (i.e. host, package/service, platform, etc) of what you’d like to accomplish?


#5

Hehe…Yeah I was trying to produce minimal code that was easy to follow, but it wasn’t very helpful! :stuck_out_tongue:

I guess a good place to start is with the result I’m looking for. I want to produce an API that looks a bit like this:

let host = LocalHost::new();
// or...
let host = RemoteHost::connect(..);

let nginx = package::factory(&host, "nginx");
// or if I have a specific package provider in mind...
let nginx = package::Yum(&host, "nginx");
nginx.install()
      start();

// Do some more stuff...

Here’s the host module:

trait Host {}

struct LocalHost;
impl Host for LocalHost {}

struct RemoteHost;
...

The RemoteHost implements tokio-proto to handle sending stuff over a socket. Currently it uses a codec based on serde-json. See actual code here: https://github.com/petehayes102/api. I’m rewriting an old project of mine.

In terms of Package's implementation, there’s currently (planned) a trait:

trait Package {
    fn available(host: &Host) -> bool;
    fn install(&self, host: &Host) -> Result<..>;
    fn uninstall(&self, host: &Host) -> Result<..>;
    ...
}

and several impl’s:

struct Yum {
    package_name: String
}

impl Package for Yum {
    fn available(host: &Host) -> bool {
        // Talk to host to discern if Yum is available or not
    }

    fn install(&self, host: &Host) {
        // If this is a `LocalHost`, actually do stuff...
        // <OR>
        // If this is a `RemoteHost`, serialize context
        // (Yum, install, package_name) and send to remote,
        // then deserialize/return result.
    }
    ...
}

struct Dnf {...}

Package is just one of several endpoints that perform functions on a Host. All the business rules are written, but the structure of the API sucks. I’m looking to improve it dramatically, whilst also making it asynchronous with Futures.

What do you think?


#6

Nothing jumps out as obviously suboptimal from the snippets you gave. Only thing is perhaps Package is commingling the notion of a PackageManager (eg yum) and a Package itself (eg nginx). So logically one might expect the following basic flow:

  1. Get a handle to a host (local or remote)
  2. Discover the package manager on it and get a handle to that
  3. Use the package manager to lookup/install/uninstall packages

Maybe the package manager shouldn’t even be visible to callers - let the host do the install/uninstall? A package manager can be an impl detail of a host.


#7

Yeah you’re right. The reason I did it that way was because some package managers have specific functions that a user might want to call manually, which are unique to that implementation. Some OSes have multiple managers too so users might want to choose one or another. The same applies for services too - systemd vs. init for instance. Thinking about it though, it’d be better to let a user compose their own Host with a specific package manager if they wish, or just have the Host choose appropriate defaults.

Thanks so much for your help and feedback mate! It’s been really valuable. :smile: