rust-magic-function-params

五月 27, 2024 [C/C++] #Rust #obsidian #编译链接

rust-magic-function-params

Rusts Axum style magic function params example

Learning Rust I met a rigid, statically typed language. Specifically it has no function overloading or optional function parameters.

Coming across Axum I was amazed to see stuff like:

let app = Router::new()
  .route("/users", get(get_users))
  .route("/products", get(get_product));

async fn get_users(Query(params): Query<Params>) -> impl IntoResponse {
    let users = /* ... */

    Json(users)
}

async fn get_product(State(db): State<Db>, Json(payload): Json<Payload>) -> String {
  let product = /* ... */

  product.to_string()
}

The get method can receive a function pointer to various types of functions! What kind of black magic is this? 🤯

I had to create a simplified version of this to figure this out.

fn print_id(id: Id) {
    println!("id is {}", id.0);
}

// Param(param) is just pattern matching
fn print_all(Param(param): Param, Id(id): Id) {
    println!("param is {param}, id is {id}");
}

pub fn main() {
    let context = Context::new("magic".into(), 33);

    trigger(context.clone(), print_id);
    trigger(context.clone(), print_all);
}

In the example we have a trigger method that receives a Context object and a function pointer. The function pointer might receive 1 or 2 parameters of the Id or Param types. Magic?

Moving parts

Lets look at the moving parts to achieve this

The context

struct Context {
    param: String,
    id: u32,
}

The Context is the received state, Request in Axums case. This is the source of the "parts" our functions want to receive. In this simplified example it contains two data fields

The FromContext trait

trait FromContext {
    fn from_context(context: &Context) -> Self;
}

The first trick is the FromContext trait. It will allow us to create "Extractors" that extract the necessary data from the context object. For example

pub struct Param(pub String);

impl FromContext for Param {
    fn from_context(context: &Context) -> Self {
        Param(context.param.clone())
    }
}

This trait will allow us to hold a Context but call a function that expects Param. More on this later

The Handler trait

trait Handler<T> {
    fn call(self, context: Context);
}

The second trick is the Handler trait. We will implement the trait for the closure typeFn(T). Yeah we can implement traits for closure types. This implementation will allow us to have a "middleware" between the function call and its arguments. Here we will call the FromContext::from_context method, converting the context to the expected function argument i.e Param or Id.

impl<F, T> Handler<T> for F
where
    F: Fn(T),
    T: FromContext,
{
    fn call(self, context: Context) {
        (self)(T::from_context(&context));
    }
}

To Support multiple function parameters we'll go ahead and implement Handler<Id> for closure types with 2, 3, 4 and so on parameters. An interesting point here is that this implementation is agnostic to the order of the parameters - it will support both fn foo(p: Param, id: Id) and fn foo(id: Id, p: Param)!

这样的话,我们在函数调用和它的参数之间就有了一个 “middleware”。这里我们将调用 FromContext::from_context 方法,将上下文转换为预期的函数参数,即 Param 或 Id。
译者注:执行 impl<F, T> Handler<T> for F 后,相当于为 Fn<T> 类型实现了 Handler 这个 trait,即 print_id 实现了 Handler,可以调用 call 方法,而 call 方法中的 self 就是 print_id。
我们继续添加代码,支持两个函数参数的情况。有趣的是,这个实现与参数的顺序无关--它同时支持 fn foo(p: Param, id: id) 和 fn foo(id: id, p: Param):
impl<T1, T2, F> Handler<(T1, T2)> for F
where
    F: Fn(T1, T2),
    T1: FromContext,
    T2: FromContext,
{
    fn call(self, context: Context) {
        (self)(T1::from_context(&context), T2::from_context(&context));
    }
}

通过宏,可以一次性实现不定参数的情况,例:

macro_rules! all_the_tuples {
    ($name:ident) => {
        $name!([]);
        $name!([T1]);
        $name!([T1, T2]);
        $name!([T1, T2, T3]);
        // Can add more
    };
}

macro_rules! impl_handler {
    (
        [$($ty:ident),*]
    ) => {
        impl<F, $($ty,)*> Handler<($($ty,)*)> for F
        where
            F: Fn($($ty,)*),
            $( $ty: FromContext,)*
        {
            fn call(self, context: Context) {
                self($( $ty::from_context(&context), )*);
            }
        }
    };
}

Putting it all together

The implementation of the trigger function is now straight forward

pub fn trigger<T, H>(context: Context, handler: H)
where
    H: Handler<T>,
{
    handler.call(context);
}

Lets examine what happens for this call

  let context = Context::new("magic".into(), 33);

  trigger(context.clone(), print_id);

Magic demystified.

附完整代码:

#[derive(Clone, Debug)]
struct Context {
    param: String,
    id: u32,
}

impl Context {
    fn new(param: String, id: u32) -> Context {
        Context { param, id }
    }
}

trait FromContext {
    fn from_context(context: &Context) -> Self;
}
trait Handler<T> {
    fn call(self, context: Context);
}

struct Param(String);

impl FromContext for Param {
    fn from_context(context: &Context) -> Self {
        Param(context.param.clone())
    }
}

struct Id(u32);

impl FromContext for Id {
    fn from_context(context: &Context) -> Self {
        Id(context.id.clone())
    }
}

macro_rules! all_the_tuples {
    ($name:ident) => {
        $name!([]);
        $name!([T1]);
        $name!([T1, T2]);
        $name!([T1, T2, T3]);
    };
}

macro_rules! impl_handler {
    (
        [$($ty:ident),*]
    ) => {
        impl<F, $($ty,)*> Handler<($($ty,)*)> for F
        where
            F: Fn($($ty,)*),
            $( $ty: FromContext,)*
        {
            fn call(self, context: Context) {
                self($( $ty::from_context(&context), )*);
            }
        }
    };
}

all_the_tuples!(impl_handler);

fn trigger<T, H>(context: Context, handler: H)
where
    H: Handler<T>,
{
    handler.call(context);
}

fn print_id(id: Id) {
    println!("id is {}", id.0);
}

fn print_param(Param(param): Param) {
    println!("id is {}", param);
}

fn print_all(Param(param): Param, Id(id): Id) {
    println!("param is {param}, id is {id}");
}

pub fn main() {
    let context = Context::new("magic".into(), 33);
    trigger(context.clone(), print_id);
    trigger(context.clone(), print_param);
    trigger(context.clone(), print_all);
}