Motivation
Functions are a special child right now in Rust and can only be implemented in traits by using unstable features [e.g once you want to call "any" function it soon gets really complicated].
And while the traits of Fn, FnOnce and FnMut work perfectly for their use case of closures, anyone that tried to do something special with functions will soon end in unstable land or has to special case to just one case.
In many cases an indirect call (to e.g. wrap async functions) can use a helper struct that can call exactly one version, but for all those helpers you need to know the exact function signature.
On the other hand you have the "named arguments, please" crowd, but again the most promising solutions (callable structs?) needs the unstable features.
Also then you have things like Generators that now need to pass a tuple in their yield, because function arguments are special, etc.
I also looked into the definition of Fn, FnOnce and FnMut and one of the things that I love about rust is that it "eats it's own dog food" in most any cases. Many things that make the language so nice are under the hood just some syntactic sugar around simple low-level concepts that are very expressive to write.
Suggested resolution
Add a FnArgs
trait that implements something like a DerefToFnArgs
that can be used everywhere where currently function arguments are used.
Instead of:
foo(arg_0, arg_1, arg_2);
you can also write
let args:FnArgs = (arg_0, arg_1, arg_2).into(); // Tuples implement the necessary FnArgs From trait automatically
foo(args);
The compiler will then de-sugar args back to:
foo(args.0, args.1, args.2);
so the run-time overhead should be zero.
Pro
One could argue that a function always works on tuples automatically and the () to call a function just means that it is a tuple:
So normally a function would be called like:
foo arguments
and the syntax
foo (arg_0, arg1)
in essence sets
arguments = (arg_0, arg_1)
automatically and then passes that as one variable to the function, which is kinda what rust-call
does magically. [I finally got why Haskell does not have parentheses around function calls!]
Future Possibilities
Named arguments are then as "simple" (still needs a macro) as:
impl From<MyArgs> for FnArgs {
fn from(args: MyArgs) -> Self {
return (args.x, args.y, args.z).into(); // Tuple can be converted to FnArgs automatically
}
}
Implementation ?
This let's us get rid of the special rust-call
syntax and use a trait for everything else.
Implementation of FnOnce might look like this:
pub trait FnOnce<Args: FnArgs> {
/// The returned type after the call operator is used.
type Output;
/// Performs the call operation.
fn call_once(self, args: Args) -> Self::Output;
}
Due to the need of Args to implement FnArgs (and I hope I got the syntax right), the compiler knows that he needs to convert any FnArgs to function arguments.
Again this is just a simple de-sugar step:
foo(args: FnArgs) => foo(args.0, args.1, args.2);
Maybe it would not even be needed to get rid of rust-call
to implement that, as it's really just one AST pre-process operation [found FnArgs in calling a function? Instead of Type-Error, de-sugar it!]. [might need more thought]
Critical
I can see that allowing variable number of arguments can lead to strange things that the compiler would need to catch and this might not yet be sound and too dynamic.
A better way might be to define FnArgs as generic on the TupleType accepted.
Then type interference can be used to derive the specific FnArgs:
let args:FnArgs<(i32, i32, i32)> = (arg_0, arg_1, arg_2).into(); // FnArgs take a tuple of type (i32,i32,i32)
foo(args);
A larger problem (and a potential dealbreaker for this proposal) might be that it is very hard to deal with &mut
mixed with non-&mut etc. in the tuples - though if the whole struct / tuple that is used as input is &mut or borrowed it might still work for those simple cases.
Critical 2
Maybe all of this is already possible as a macro and should not even be in standard lib at all?
Final words
I might have gotten some concepts / implementations wrong [I am still new to Rust], this pre-RFC is presented for the idea of not special casing rust-call
and/or allowing a specialized Tuple to be accepted as fn arguments as if the user had split it up manually.
Please send me feedback!