Mismatched match arms with futures

#1

I’ve been trying to get some asynchronous device I/O stuff working using futures. However, I’m running into a wall and I’m not sure how to fix this. I apologize for the verbosity of this question and I hope that I can actually communicate my question effectively.

Here’s some background information about my types:

// BACKGROUND TYPE INFORMATION:
pub struct DeviceConfig<T: AsyncHidDevice<MidiFader>> {
    ... fields, including a T ...
}

impl<T: AsyncHidDevice<MidiFader>> DeviceConfig<T> {
    pub fn new(device: T) -> impl Future<Item=Self, Error=Error> {
        ...futuristic stuff that asynchronously talks to T...
    }
}

Basically, I’ve got a struct called DeviceConfig which embodies the configuration read back from a particular hardware USB device (the device is embodied by a struct which implements AsyncHidDevice<MidiFader>. The exact struct used varies by platform, since that manages I/O handles, etc). All that seems to work fine.

The issue I’m having stems from the interaction with between my asynchronous I/O and a GUI. I couldn’t figure out a good way to get a gui to interact well with tokio, so I decided to go with a message-passing approach where the GUI would send “Requests” containing the T: AsyncHidDevice<MidiFader>> and get back “Response” objects that pass the T: AsyncHidDevice<MidiFader> back over.

I’ve got a function (configure) that looks like this and I’m hoping will perform the message passing operations:

/// Configuration request
pub enum Request<T: AsyncHidDevice<MidiFader>> {
    ReadConfiguration(T),
    WriteConfiguration(DeviceConfig<T>),
}

/// Configuration response
pub enum Response<T: AsyncHidDevice<MidiFader>> {
    Easy,
    Configured(DeviceConfig<T>),
}

pub fn configure<T: AsyncHidDevice<MidiFader>>(
    requests: mpsc::Receiver<Request<T>>, responses: mpsc::Sender<Response<T>>) -> impl Future<Item=(), Error=Error> {

    requests.map_err(|e| e.into())
        .for_each(|r: Request<T>| {
            match r {
                Request::ReadConfiguration(dev) => {
                    DeviceConfig::new(dev)
                        .and_then(|c| responses.send(Response::Configured(c)).map_err(|e| e.into()))
                },
                Request::WriteConfiguration(_) => responses.send(Response::Easy).map_err(|e| e.into()),
            }
        })
}

It uses requests as a Stream and uses for_each to perform some tasks on the request. There is an obvious problem here: The match arms don’t result in the same type! This obviously makes some compile errors:

error[E0308]: match arms have incompatible types
   --> src/config.rs:478:13
    |
478 | /             match r {
479 | |                 Request::ReadConfiguration(dev) => {
480 | |                     DeviceConfig::new(dev)
481 | |                         .and_then(|c| responses.send(Response::Configured(c)).map_err(|e| e.into()))
482 | |                 },
483 | |                 Request::WriteConfiguration(_) => responses.send(Response::Easy).map_err(|e| e.into()),
    | |                                                   ---------------------------------------------------- match arm with an incompatible type
484 | |             }
    | |_____________^ expected struct `futures::AndThen`, found struct `futures::MapErr`

Even if they were both AndThen, they would be different types. So, I tried using boxes:

fn read_configuration<T: AsyncHidDevice<MidiFader>>(dev: T, responses: mpsc::Sender<Response<T>>) -> Box<Future<Item=(), Error=Error>> {
    Box::new(DeviceConfig::new(dev)
        .and_then(move |c| responses.send(Response::Configured(c)).map_err(|e| e.into()))
        .and_then(|_| Ok(())))
}

But this also doesn’t work:

error[E0310]: the parameter type `T` may not live long enough
   --> src/config.rs:467:5
    |
466 |   fn read_configuration<T: AsyncHidDevice<MidiFader>>(dev: T, responses: mpsc::Sender<Response<T>>) -> Box<Future<Item=(), Error=Error>> {
    |                         -- help: consider adding an explicit lifetime bound `T: 'static`...
467 | /     Box::new(DeviceConfig::new(dev)
468 | |         .and_then(move |c| responses.send(Response::Configured(c)).map_err(|e| e.into()))
469 | |         .and_then(|_| Ok(())))
    | |______________________________^
    |
note: ...so that the type `futures::AndThen<futures::AndThen<impl futures::Future, futures::MapErr<futures::sink::Send<tokio::sync::mpsc::Sender<config::Response<T>>>, [closure@src/config.rs:468:76: 468:88]>, [closure@src/config.rs:468:19: 468:89 responses:tokio::sync::mpsc::Sender<config::Response<T>>]>, std::result::Result<(), config::Error>, [closure@src/config.rs:469:19: 469:29]>` will meet its required lifetime bounds
   --> src/config.rs:467:5
    |
467 | /     Box::new(DeviceConfig::new(dev)
468 | |         .and_then(move |c| responses.send(Response::Configured(c)).map_err(|e| e.into()))
469 | |         .and_then(|_| Ok(())))
    | |______________________________^

I can’t add a 'static due to reasons (for one, the AsyncHidDevices has a handle which is opened at runtime…so…). So, I could add a different explicit lifetime parameter and probably get that function to compile, but I strongly suspect I’m going to run into further issues when these lifetimes interact with the mpsc channels. I also suspect that this will cause the match arm problem all over again because the lifetimes might not match. I was passing the concrete objects around so that I could avoid specifying lifetimes and Box seems to negate that.

Questions:

  1. Going back through my tree and redesigning it to have lifetime parameters is a large time investment for a hobby project. I’ve already spent 3x longer on this aspect of the project (writing the GUI in rust) than it took me to design and fabricate the circuit board for the device this GUI is for, write firmware (in C) complete with a A-B partitioned configuration memory area, and write a self-reprogramming USB bootloader. Should I be going back and annotating everything with lifetimes just so I can dynamic-dispatch a Future in this particular case? Is this the only way to get this working?
  2. Is Box the only way to overcome this match arm problem when the arms return futures that have different paths? Or to not use the and_then, map_err, etc convenience functions and instead implement the future with two different underlying state machines so both arms return that object (probably an Enum than implements future)? This is incredibly un-ergonomic.

I’m now attempting to use mio without futures, but I imagine that will be even less ergonomic than what I have here.

0 Likes

#2

Have you tried Either?

1 Like

#3

Oh man!!!

Finished dev [unoptimized + debuginfo] target(s) in 12.57s

It’s like someone thought of this situation and came up with Either to cover it. Thanks for the tip!

0 Likes