Function Overloading in Rust

Why you shouldn’t even think about doing it

Nikodem Rabuliński
The Startup
4 min readJul 15, 2020

--

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:

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.
  • 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 Trait in 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:

I am using my functionate macro here but by the time you read this I might’ve added a feature to automatically create semi-overloaded-functions to it

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 FnOnce, FnMut, and Fn respectively.

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 str (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.

--

--