David Sackstein
David Sackstein

Reputation: 560

How can I extract a part of function in Rust when the return type of the extracted function is defined in terms of a trait?

The Problem

I am using Rust with vscode and the plugins brought in by "Rust and Friends v1.0.0".

I would like to refactor a long function using the Extract Function technique but in some cases the IDE is not able to figure out the return type of the extracted function.

I think the reason is that the type is described in terms of a trait and it is not possible to define that type as a return type.

As I am new to Rust and I expect that my assessment is not accurate I will provide an example.

Example

I am using the paperclip crate to set up a REST server. The part that configures the server looks like this:

let server = HttpServer::new(move || {
    let app = App::new()
        .wrap(Logger::default())
        .wrap_api()
        .data(pool.clone());

    let app = app.service(
        web::scope(“/api”).service(
            web::scope(“/customers”).service(
                web::resource(“/transactions”)
                    .route(web::get().to(schema_handlers::get_transactions))
                    .route(web::post().to(schema_handlers::add_transaction)),
            ),
        ),
    );
    
    let app = app.service(
        web::scope(“/api”).service(
            web::scope(“/admin”).service(
                web::resource(“/permissions”)
                    .route(web::get().to(schema_handlers::get_permissions))
                    .route(web::post().to(schema_handlers::add_permission)),
            ),
        ),
    );

    app.with_json_spec_at("/api/spec").build()
})
.bind(format!("0.0.0.0:{}", port))?
.run();

paperclip supports a fluent API so that all service definitions could be chained, but I would prefer to extract a function for each scope of handlers that I am adding.

This is why I initially split the single fluent call into two separate assignments.

The next step is to extract each let app = app.service ( statement into a function.

But to do so, I need to be able to express the type of app or at least the name of a trait that exposes the service method as it is being used here.

In this case, the IDE fails to detect the type.

When I use the The “let” type trick in Rust and some of the hints in the IDE I conclude that the type is:

App<impl ServiceFactory<Config = (), Request = ServiceRequest, Response = ServiceResponse<StreamLog<Body>>, Error = Error, InitError = ()>, StreamLog<Body>>

This type cannot be used explicitly to qualify the app variable nor can it be used as the return type of an extracted function that would replace the right side of the assignment to app.

From the compiler error messages I understand that the presence of a trait in the type expression (as indicated by the presence of the impl keyword is the cause of this problem.

Another issue is that this type specification is very long and verbose.

I could solve the verbosity by a type alias, but the compile complains that impl is not stable in type aliases, which sounds to me as boiling down to the same problem.

Learned from the Example

It seems to me that there are cases in which types are well defined and can be inferred by the compiler but, because they contain trait definitions, they cannot be (easily) written explicitly and therefore the extract function method of refactoring is not always possible.

This seems to me a significant limitation of the language.

Is there a way I can extract functions today (without waiting for trait aliases in Rust)?

Upvotes: 2

Views: 556

Answers (1)

kmdreko
kmdreko

Reputation: 60472

I'll assume you wanted the refactoring to change this:

let app = app.service(
    web::scope("/api").service(
        web::scope("/customers").service(
            web::resource("/transactions")
                .route(web::get().to(schema_handlers::get_transactions))
                .route(web::post().to(schema_handlers::add_transaction)),
        ),
    ),
);

into something like this:

fn add_transaction_routes(app: App) -> App {
    app.service(
        web::scope("/api").service(
            web::scope("/customers").service(
                web::resource("/transactions")
                    .route(web::get().to(schema_handlers::get_transactions))
                    .route(web::post().to(schema_handlers::add_transaction)),
            ),
        ),
    )
}

let app = add_transaction_routes(app);

Of course this doesn't work since App is generic and isn't complete. You can use impl Trait for the parameters and return type like so:

fn add_transaction_routes(
    app: App<impl ServiceFactory<...>, Body>,
) -> App<impl ServiceFactory<...>, Body> {

but I would consider this a slight misuse. While it may not be incorrect per se, the impl Traits are deduced separately. The function signature conveys that the app that is passed in might not be the same type that is returned, but .service() actually returns Self. So it'd be more appropriate to make this a simple generic function:

fn add_transaction_routes<T>(app: App<T, Body>) -> App<T, Body>
where
    T: ServiceFactory<...>,
{

You can then choose to make a supertrait to reduce the boilerplate required to call .service():

ServiceFactory<
    ServiceRequest,
    Config = (),
    Response = ServiceResponse<Body>,
    Error = Error,
    InitError = (),
>

But this is all starting to get messy. While its possible, its clear that this type of organization is not particularly designed for. Instead I propose your refactorings is around making the services instead of around adding them to the App. I think this is much clearer:

fn transaction_routes() -> impl HttpServiceFactory {
    web::scope("/api").service(
        web::scope("/customers").service(
            web::resource("/transactions")
                .route(web::get().to(schema_handlers::get_transactions))
                .route(web::post().to(schema_handlers::add_transaction)),
        ),
    )
}

let app = app.service(transaction_routes());

Upvotes: 6

Related Questions