Function Overloading in Rust
Why you shouldn’t even think about doing it
Around half a year ago I started experimenting with the idea of implementing
Fn* traits on custom types. I didn’t have enough knowledge or experience in Rust to really make anything with it but now that I have, I present to you — true function overloading in Rust. So you don’t have to come up with this abomination yourself.
Before we begin
I won’t be discussing
Fn* traits or implementing them in this post — I’ll do so in another article. Rather, this will cover creating the illusion of function overloading, and showing the power of some of Rust’s unstable features in the process.
How many times have you wanted to write code like this but found the compiler complain about foo being redefined?
Probably never. Well, maybe once, but now you can go around this limitation and write the worst Rust code possible! Isn’t that great?
First, we need to choose the nightly toolchain and enable a few unstable features. If that doesn’t raise any flags for you already, let me tell you: One of them is incomplete and may not be safe to use or cause compiler crashes. Sound fun? Great.
To enable all features, add the following line to the top of your root file:
#![feature(unboxed_closures, fn_traits, type_alias_impl_trait, impl_trait_in_bindings)]
Let’s discuss why we need each of those.
unboxed_closures— This feature allows us to write functions using the
"rust-call"ABI, required for implementing the
fn_traits— As the name suggests, this allows us to implement
Fn*in the first place.
type_alias_impl_trait— This one is a bit trickier, we need it to be able to use
impl Traitin associated types, we’ll discuss why we need that later.
impl_trait_in_bindings— The key to our “true” function overloading, so far the most unstable feature, may cause the compiler to crash.
Now for the part we’ve all been waiting for
I’ll show you the code and then explain how and why it works:
That’s a weird Rust piece of code. And what does this macro do behind the scenes? Well, it expands to this:
That’s a lot of repeated code, hence I opted for using my macro for automating this process.
The macro starts by creating a trait for each method in the
impl block. That allows to define an associated type for each return type. Associated types allow using the
impl Trait in return types for methods which either already have
impl Trait as the return type, or are
async methods—the same as a method with a return type of
impl Future. They’re needed to let the compiler determine the actual return type. (Thanks to this article for showing this workaround)
The macro then repeats that process, implementing
Struct definition, implementation and initialization gets enclosed in a block so that anything outside of said block only knows about that single instance.
This is what creates the illusion of an overloaded function
The block gets assigned to a static variable of which type we specify as
impl followed by all the
Fn implementations our “anonymous” struct has.
In this example it’s a function which takes no arguments and returns a
Fn() -> &’static str) but also a function which takes a single integer argument and returns an integer (
Fn(i32) -> i32).
Thanks to that
foo is almost no different from a closure to any code that uses this static.
And does it work?
It does! (obviously, I wouldn’t write this if it didn’t) We can use
foo like a function, pass it as an argument and anything else one can do with a regular closure.
Now let’s answer the question
Why shouldn’t I use this?
Or any other anti-patterns, tricks, and hacks like this
I already answer this question in the
README of my functionate crate but allow me to reiterate:
You might think it’s a good idea to do this, to bundle multiple implementations which accept different arguments into a single function but in reality it’s much worse than you can imagine.
First and foremost — readability (or lack thereof). I can guarantee you that if you were to implement something like this in your codebase, absolutely no one would understand what is going on and even you would start to question why you did this if you came back to it after a week or two.
Secondly, it requires unstable features. While using nightly for a hobby project, an experiment or in the real world with good justification and precautions in place isn’t wrong, you should absolutely never rely on incomplete features in production code or in public libraries as it may lead to unexpected crashes of the program or the compiler itself.
Even if all those features get stabilized (doubt), you’re still introducing anti-patterns and non-idiomatic, confusing code, which should be one of the things you avoid at all cost.
Thanks for reading! I hope you enjoyed this journey as much as I did.