Associated types on enum variants in rust

960 Views Asked by At

Imagine the following scenario: an Application can accept different kind of requests, processes them and returns either a response or and error. The different kinds of requests should be variants of an enum, such that we can match on them in a processReqest function. However each request type should have its own associated response and error types. How can we achieve this in Rust?

trait ReqKind {
    type ResponseType;
    type ErrorType;
}

struct ImgReq {}
struct ImgRes {}
struct ImgErr {}

impl ReqKind for ImgReq {
    type ResponseType = ImgRes;
    type ErrorType = ImgErr;
}

struct TextReq {}
struct TextRes {}
struct TextErr {}

impl ReqKind for TextReq {
    type ResponseType = TextRes;
    type ErrorType = TextErr;
}

enum Requests {
    Img(ImgReq),
    Text(TextReq),
}

fn processReqest(r: Requests) -> ??? {
    match r {
        Requests::Img(_) => {
            return Ok(ImgRes);
            // or return Err(ImgRes)
        }
        Requests::Text(_) => {
            return Err(TextErr);
            // or return Ok(TextRes)
        }
    }
}

Here is what I have so far, but I don't know how we would specify the return type of the processRequest function.

3

There are 3 best solutions below

1
cafce25 On

Unless you're using trait objects you can't return different types from a function depending on the input, since you're already using static dispatch for your input you can just unify the return types like you did with the Requests enum:

enum Response {
    Img(ImgRes),
    Text(TextRes),
}

enum Error {
    Img(ImgErr),
    Text(TextErr),
}

fn processReqest(r: Requests) -> Result<Response, Error> {
    match r {
        Requests::Img(_) => {
            return Ok(Response::Img(ImgRes));
            // or return Err(ImgRes)
        }
        Requests::Text(_) => {
            return Err(Error::Text(TextErr));
            // or return Ok(TextRes)
        }
    }
}

Or to make use of the associated types just make your function a method on the trait instead:

trait ReqKind {
    type Response;
    type Error;
    fn process_request(&self) -> Result<Self::Response, Self::Error>;
}

impl ReqKind for ImgReq {
    type Response = ImgRes;
    type Error = ImgErr;
    fn process_request(&self) -> Result<Self::Response, Self::Error> {
        Ok(Self::Response)
    }
}

impl ReqKind for TextReq {
    type Response = TextRes;
    type Error = TextErr;
    fn process_request(&self) -> Result<Self::Response, Self::Error> {
        Err(Self::Error)
    }
}
4
Chayim Friedman On

Unfortunately, you need to generate an enum to for the error and for the response yourself. If there are many variants, or lots of enums, you can use the following macro to auto-generate them (it is not perfect, but it does the job and it is extendable):

macro_rules! generate_enum {
    {
        enum $name:ident ( $res_name:ident, $err_name:ident ) {
            $(
                $variant:ident ( $variant_ty:ty )
            ),* $(,)?
        }
    } => {
        enum $name {
            $(
                $variant ( $variant_ty ),
            )*
        }
        enum $res_name {
            $(
                $variant ( <$variant_ty as ReqKind>::ResponseType ),
            )*
        }
        enum $err_name {
            $(
                $variant ( <$variant_ty as ReqKind>::ErrorType ),
            )*
        }
        impl ReqKind for $name {
            type ResponseType = $res_name;
            type ErrorType = $err_name;
        }
    };
}

generate_enum! {
    enum Requests(RequestsRes, RequestsErr) {
        Img(ImgReq),
        Text(TextReq),
    }
}

fn processReqest(r: Requests) -> Result<RequestsRes, RequestsErr> {
    match r {
        Requests::Img(_) => {
            return Ok(RequestsRes::Img(ImgRes {}));
            // or return Err(ImgErr)
        }
        Requests::Text(_) => {
            return Err(RequestsErr::Text(TextErr {}));
            // or return Ok(TextRes)
        }
    }
}
0
fubupc On

An alternative approach is to discard enum Requests and convert processRequest to a ReqKind method:

trait ReqKind {
    type ResponseType;
    type ErrorType;

    // Add this `process` method to replace original *polymorphic* `processRequest` method
    fn process(&self) -> Result<Self::ResponseType, Self::ErrorType>;
}

struct ImgReq {}
struct ImgRes {}
struct ImgErr {}

impl ReqKind for ImgReq {
    type ResponseType = ImgRes;
    type ErrorType = ImgErr;

    fn process(&self) -> Result<Self::ResponseType, Self::ErrorType> {
        return Ok(ImgRes {});
        // or return Err(ImgRes)
    }
}

struct TextReq {}
struct TextRes {}
struct TextErr {}

impl ReqKind for TextReq {
    type ResponseType = TextRes;
    type ErrorType = TextErr;

    fn process(&self) -> Result<Self::ResponseType, Self::ErrorType> {
        return Ok(TextRes {});
        // or return Err(ImgRes)
    }
}

It should works in scenario that concrete types such as ImgReq, TextReq can be determined at compile-time. Using enum Requests to wrap them actually loses compile-time type information and has to check it at runtime.