How to understand complex function chaining in Rust

I am new to Rust. I went through tutorials/play with several small projects and feel comfortable with straightforward code.

However, there is one thing where I constantly get stuck. It's more of a question of how to understand Rust (vs solving specific programming problem).

The place where I get stuck is understanding what should I pass and return from some (complex) functions exposed by some libraries.

Just to give you an example hyper.rs has two functions:

  • make_service_fn
  • service_fn

Hello world (using these function may look something like this):

use std::{convert::Infallible, net::SocketAddr};
use hyper::{Body, Request, Response, Server};
use hyper::service::{make_service_fn, service_fn};

async fn handle(_: Request<Body>) -> Result<Response<Body>, Infallible> {
    Ok(Response::new("Hello, World! {}".into()))
}

#[tokio::main]
async fn main() {
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    let make_svc = make_service_fn(|_conn| async {
        Ok::<_, Infallible>(service_fn(handle))
    });

    let server = Server::bind(&addr).serve(make_svc);

    if let Err(e) = server.await {
        eprintln!("server error: {}", e);
    }
}

And here, two things that puzzle me:
a) That we wrap everything in Ok() in handle function
b) That we wrap Ok::<_, Infalliable> inside make_service_fn

Obviously, it’s needed somewhere in .serve(). However, I can’t seem to find a good way to understand what is exactly expected by serve? What make_service_fn should take as an argument and return? What service_fn should take as an argument and return?

I can look at the documentation of each function. However, they don’t talk about a need to have Result (vs different struct or enum).

And if I try to change the code and remove Result<> and Ok wrapping, it complains about quite strange (for me) errors in a couple of places.

This is not the only place where I got stuck with something like that. It feels like some libraries/frameworks have a complicated mix of traits, generics, trait bounds, sprinkled with Futures. The beauty and the curse that the language syntax hides all of this (that Rust compiler figures out under the hood which types each thing are).

The question which I have. How do you untangle these things? I tried to start from each end and completely got lost in a level of indirections and things wrapped one into each other. It feels like I am trying to solve it using a method that works well on other languages but doesn't work well on Rust libraries.

Any thoughts/recommendations?

TBH it's not easy to use hyper directly, and it's not even their priority ATM. As stated in their document the hyper is meant to be a low-level building block for high level frameworks. If you want to build a http server based on it, try axum first.

I can understand why you're confused. Hyper is designed to allow for a lot of flexibility with maximum performance. That leads to a slightly complicated API unfortunately.

Given you're new to Rust I think @Hyeonu's advice is correct. Don't try to understand everything all at once. Try one of the higher level frameworks built on hyper such as auxm (full disclosure: I'm the maintainer of axum), warp, rocket, tide, or actix-web. And once you're comfortable with the basic then slowly dive into more advanced things.


To answer some concrete questions you had:

a) That we wrap everything in Ok() in handle function

The Ok(_) means that handling the request succeeded. If something failed we could have returned Err(some_error).

b) That we wrap Ok::<_, Infalliable> inside make_service_fn

This is the same thing. Hyper works by you giving it a "make service" which is basically an asynchronous functions that produces a handler (ahem "service") for a single connection. Imagine that handling a connection requires first grabbing some data about the connection, maybe the address or something. That can also fail which is why we return Ok(_).

However doing Ok(_) alone doesn't tell rust what the error type. Remember a value of for example Ok(true) has type Result<bool, ???>. We have to tell rust what ??? is. That is what the turbofish syntax with Ok::<_, Infallible>(_) does. That tells rust the error type is Infallible.

The reason all this business isn't necessary in the handle function is because rust can infer the error type from the function signature.

@davidpdrsn Thank you. Yeah. Starting slow and gradually getting into more complex things is better to approach with Rust. I think, I so got used to languages where after going through tutorials, you can read and comprehend about 80-90% of code.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.