Search code examples
rustmetaprogramming

Mapping a type to another type in rust


I'm trying to write a rust (meta-)function that maps some input type to some unrelated output type.

Coming from a C++ background, I would usually write it like this:

template<typename T>
struct f;

template<>
struct f<int> { using type = double; };

using input_type = int;
using output_type = f<input_type>::type;

My naive attempt to write the same in rust looks like this:

macro_rules! f {
  ($in: ty) => {
    match $in {
      i32 => f32,
    }
  }
}

type OutputType = f!(i32);

but, well, this fails to compile because the macro apparently doesn't return a type.

$ rustc typedef.rs 
error: expected type, found keyword `match`
 --> typedef.rs:3:5
  |
3 |     match $in {
  |     ^^^^^ expected type
...
9 | type OutputType = f!(i32);
  |                   -------
  |                   |
  |                   this macro call doesn't expand to a type
  |                   in this macro invocation
  |

What's the idiomatic rust way to map one type to another?


Solution

  • The reason your macro doesn't work is because of match. Your code type OutputType = f!(i32); will expand to:

    type OutputType = match i32 {
        i32 => f32,
    };
    

    But you can't match over types. match only works for values. However, macro_rules! itself already has a pattern matching feature that operates on tokens. So you could write the macro like this:

    macro_rules! f {
      (i32) => { f32 };
      (i64) => { f64 };
    }
    
    type OutputType = f!(i32);
    

    But this is still very far away from your C++ example! A macro just operates on its input tokens meaning that this only works with a literal match. For example, this just doesn't work:

    fn foo<T>() {
        let _: f!(T) = todo!();
    }
    

    This results in "error: no rules expected the token T".

    To have a type-level function from one type to another, you want to use traits in Rust. For example:

    trait F {
        type Out;
    }
    
    impl F for i32 {
        type Out = f32;
    }
    
    impl F for i64 {
        type Out = f64;
    }
    
    type OutputType = <i32 as F>::Out;
    
    fn foo<T>() {
        // This works now.
        let _: <T as F>::Out = todo!();
    }
    

    There is a lot more to say about using traits like this. The whole system is Turing complete and people built lots of stuff in it. But this should suffice for this Q&A.