However, in Rust, I cannot think of anyway to do that. Like so:
let my_vec = vec![1,2,3,4,5];
let median = median!(&my_vec);
When I tried doing that, I get a syn::Ident with value "my_vec".
Generally, what is the mechanism/idiom to pass reference/value into the procedural macro and let the macro treats them as reference/value respectively?
To address this point specifically: this is impossible. The compiler simply doesn't have that information at the point macros are expanded. The macro gets the tokens you pass to it, and that's it. You can't check types or look at values because none of it has been determined yet.
A macro can't compute the median of a sequence, because it has no access to the sequence. The sequence doesn't even exist at the point in compilation where the macro is expanded - it's working with source code, whereas the Vec created by the vec! macro will only exist at runtime. A macro could, at best, expand to code that will evaluate the median when run, but it's not really obvious why that would be an improvement over a function.
The way I'd do this kind of overloaded function in Rust would be to define a function generic in <T>, that receives a Vec<T>, where T is constrained to types which implement Ord or PartialOrd. That function can be free-standing (median(&my_vec)), or defined via a trait as a method on vectors (my_vec.median()). Something like:
fn median<T>(vec: &[T]) -> Option<T>
where T: PartialOrd
{
// left as an exercise
}
Why is it that you want to use a macro instead of a function for this?
Rust doesn't have a close equivalent to C++ metaprogramming. On the flip side, C++ doesn't have a close equivalent to Rust macro metaprogramming. There's some overlap in capability, but this particular case doesn't hit that overlap unless you reformulate it:
You may be looking for this function if you want access to the length:
fn median<T, const LEN>(ary: &[T; LEN]) -> Option<&T>
where T: PartialOrd,
[T; LEN]: Sized /* actually a wf constraint but i don't want to be confusing */
{
}
The reason why I am using a macro and not a const fn is because in C++, I am able to do constexpr std::conditional<(N % 2 == 0), float, int> such that the return type of the median C++ function is different depending on the number of the arguments passed into it.
My Rust macro is able to return i32 or f64 depending on the number of arguments passed, but seems like it is unable to evaluate if the argument passed in is an array or a vector. But well, certainly I am aware of const fn calculating median at compile time.
In short, I am trying to achieve two things:
Computation of median at compile time. Both C++ and Rust are able to do it.
Return different types based on the number of arguments passed in, i.e. if Even, return float/f64 else int/i32. Both C++ and Rust are able to do it.
I believe the prevailing view among the Rust community is that attempting this leads quickly to madness.
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 could start writing that up, but there's a problem: we don't really want this function to be returning i32 or f64. We want the function to be returning {integer} or {float} (the unresolved type of integer & float constants in the source code before they're resolved into a concrete type). To do that, macros are mandatory.
Also you can't write completeness proofs for const generic arithmetic yet.
trait MedianOutput {
type Output;
}
impl<T> MedianOutput for [T; 0]
where T: num_traits::Num
{
type Output = T;
}
impl<T> MedianOutput for [T; 1]
where T: num_traits::Num,
f64: From<T>
{
type Output = f64;
}
impl<T> MedianOutput for [T; 2]
where T: num_traits::Num
{
type Output = T;
}
impl<T> MedianOutput for [T; 3]
where T: num_traits::Num,
f64: From<T>
{
type Output = f64;
}
// repeat until you reach 12
fn main() {
let _x: <[i32; 3] as MedianOutput>::Output;
}
It means exactly what it says. f64 must implement From<T> for the given T. This still constrains T, not f64.
Bounds don't constrain the LHS – they constrain the appearing type variables. Just like the inequality 1 < x doesn't constrain the number 1, it constrains the variable x.
The "array" of numbers is represented as a type-level linked list of the form (i64, (i64, (…, (…, i64)))). That step recursively implements the trait for every such list.
That's the whole point of the entire implementation, in fact. The definition of its associated type:
type Median = <T::Median as Invertible>::Inverse;
ensures that a list of length N+1 has a median of type f64 if the median of the list of length N has a median of type i64, and vice versa.
Yeah, I noted that is the genius part of your solution! It flips the associated type of Invertible with every increment of the length of the sequence passed into the declarative macro list!(...). For i64, its Invertible::Inverse is i64, which serves as the base case, and each subsequent increment flips the associated type once, hence for a sequence of length N, it flips N-1 times. If N is even, N-1 will be odd and hence the flips end up in f64 and vice versa.