Rust's Equivalent of Metafunctions

In Jody Hagins' 2020 talk at Cppcon [link], he gave the following example of a metafunction as follows:

template <typename T>
struct remove_volatile : TypeIdentity<T> {};

template <typename T>
struct remove_volatile<T volatile> : TypeIdentity<T> {};

template <typename T>
using remove_volatile_t = typename remove_volatile<T>::type;

He mentioned, not exactly but along the lines of, that we can treat a metafunction as a function that operates on types where in struct remove_volatile<T volatile> : TypeIdentity<T> {}; we can sort of think of T volatile as the input to our metafunction remove_volatile and the output we get is TypeIdentity<T>. To invoke this metafunction, we can use, as an example remove_volatile_t<int volatile>.

In the comment on this topic, @riking mentioned the following:

Rust type-level functions take the form of traits and associated types.
The trait is the "function", the type it's implemented on and
the generic parameters are the "inputs",
and the associated types are the "outputs".

I am inquisitive if there is a 1-1 correspondence between C++ metafunctions and Rust's traits and if so, what are the correspondences?

Rust type-level functions take the form of traits and associated types.

The correspondence is largely accurate, as for anything more non-trivial than simple abbreviation/aliases, you’ll need to involve traits.

The correspondence is however not 1-1, as C++’s approach to working with type parameters is quite different in various ways. For starters, the analogue of something like remove_const doesn’t exist Rust for reasons such as Rust not having types like const int in the first place. If you distinguish mutability in Rust, it’s either variables that are mut or not, but this doesn’t alter their types; or you can distinguish references &i32 vs &mut i32 or pointers *const i32 vs *mut i32, but those don’t mean mut i32 is a type, but &, &mut, *const, and *mut are syntax you cannot decompose into parts meaningfully, and they simply create distinct reference or pointer types.

2 Likes

There can't be a 1:1 correspondence because C++ has SFINAE and Rust does not yet have either specialization or negative bounds. As a consequence, any "remove" has to be written over an exhaustively specified set of types — for example, you could write a trait that takes i32, f32, &i32, &&&&f32, etc. and produces the numeric type with all & deleted, but you can't do that for arbitrary &T to T.

Here is an example of a “remove a property” sort of type-level function that you can currently write:

trait UnMut {
    type Output;
}
impl<'a, T: ?Sized> UnMut for &'a T {
    type Output = &'a T;
}
impl<'a, T: ?Sized> UnMut for &'a mut T {
    type Output = &'a T;
}

This takes any reference and always produces the immutable version. It would be “called” like <T as UnMut>::Output. No promises that's useful — it's just an example that came to mind looking at yours.

You can look at “ordinary” traits like ToOwned as being type-level functions: "given this possibly unsized, only-borrowable type, give me the owned version of it”. In addition to transforming the type it also provides an actual function to convert from the input type to the output type.

Also, a trait which has at least one type parameter (trait Foo<T>) can be seen as a family of type-level functions, or a type-level “generic function” in the Common Lisp sense — every time you define a new type and implement the trait for it, you get to choose a new set of output associated types that obeys the signature defined by the trait:

trait Wrap<W> {
    type Output;
    fn wrap(self) -> Self::Output;
}

struct Foo<T>(T);
struct Fooify;
impl<T> Wrap<Fooify> for T {
    type Output = Foo<T>;
    fn wrap(self) -> Self::Output {
        Foo(self)
    }
}

struct Bar<T>(T);
struct Barify;
impl<T> Wrap<Barify> for T {
    type Output = Bar<T>;
    fn wrap(self) -> Self::Output {
        Bar(self)
    }
}

In this case, the types Fooify and Barify identify two different type-level functions, Wrap<Fooify> and Wrap<Barify>.

6 Likes

I slightly modified that example, just for the fun of it:

trait Reference {
    type Target: ?Sized;
    const IS_MUT: bool;
    type AsShared;
    fn reborrow_shared(self) -> Self::AsShared;
}

impl<'a, T: ?Sized> Reference for &'a T {
    type Target = T;
    const IS_MUT: bool = false;
    type AsShared = &'a T;
    fn reborrow_shared(self) -> Self::AsShared {
        self
    }
}
impl<'a, T: ?Sized> Reference for &'a mut T {
    type Target = T;
    const IS_MUT: bool = true;
    type AsShared = &'a T;
    fn reborrow_shared(self) -> Self::AsShared {
        self
    }
}

fn foo<T: Reference>(reference: T) -> T::AsShared {
    reference.reborrow_shared()
}

fn main() {
    let mut s = "Hello".to_string();
    let ref_mut: &mut str = &mut s;
    let _ref_shared = foo(ref_mut);
    //let _: &mut str = _ref_shared; // this would fail
}

(Playground)

Probably not very useful, but who knows.

Hi Sir, just curious how would the trait Unmut be invoked. Because seems like it cannot be simply invoked with e.g. <Self as Unmut>::Output etc.

What did you try? Here are some examples.

You can do something like this:

struct Ref<T: Unmut> {
  reference: <T as Unmut>::Output,
}

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.