Nate Clark

Software Developer

Educator

Vanity Rust - Implementing Fn on Rust Structs

February 09, 2020

The other night I was doing a coding challenge on codewars. It didn’t really have anything to do with algorithms. Instead it focused on your ability to construct a specific API. On codewars, the challenge is only available in Python, JavaScript, and Ruby, but I thought it would be fun to try to solve it with Rust. It turns out that I needed to use some nightly Rust features, so I thought I’d share. The challenge can be summed up in the following line:

assert_eq!(seven(times(five())), 35);

Just from a type signature level, we can determine a few things:

  • We need some “number” functions (seven and five) and some “operator” functions (times).
  • The “number” functions need to be able to be called with 0 arguments or 1 argument.
  • The “operator” functions will always be called with 1 argument

In this post, I want to focus on how you would go about implementing the number functions.

Depending on your language of choice you probably end up googling one of two topics: function overloading, or default arguments. If you were to do so, you would quickly find several kind posts about how Rust doesn’t support either of these. People will suggest accepting an Option argument or simply creating multiple functions. These are both great suggestions for real software, but this is about code golfing. So instead… we’re going nightly.

Nightly Rust

$ rustup override set nightly

But we’re not just doing it for fun, we’re doing it because we need two specific nightly features: unboxed_closures (docs), and fn_traits (docs).

Unboxed Closures

unboxed_closures gives us the ability to reach into a lower level of function calling logic. This will feel somewhat similar to defining a variadic function (e.g. using arguments or rest parameters in JavaScript). Let’s write a simple add function in Rust normally, and then using unboxed_closures.

// normal
fn add(x: isize, y: isize) -> isize {
    x + y
}
#![feature(unboxed_closures)]
// with unboxed_closures
extern "rust-call" fn add(args: (isize, isize)) -> isize {
    args.0 + args.1
}

However, there is a catch. The unboxed_closures version of add technically has a single argument that is a tuple. This is important to us, because we have to use unboxed_closures to implement a Fn trait for our struct.

Implementing Fn

Let’s go ahead and dump out a struct to box up our number. In just a minute we’ll write the required code to treat our structs like functions.

struct Number {
    number: isize
}

const five: Number = Number { number: 5 };
const seven: Number = Number { number: 7 };

Okay, so we have these structs that box up an integer. Now we need to let Numbers act like functions. Additionally, we need it to act like a different function depending on the arguments (function overloading). We’ll do this by implementing the FnOnce trait for Number. FnOnce requires that we provide an Output type and a call_once method that is an unboxed_closure. Considering we need it to be overloaded, we will provide an implementation for each argument configuration. Note: In my second implementation, I use an argument type of Operation. I’m not defining Operation here, but it is part of how solved the original coding challenge.

#![feature(fn_traits, unboxed_closures)]

// When called with 0 arguments
impl FnOnce<()> for Number {
    type Output = isize;
    extern "rust-call" fn call_once(self, args: ()) -> Self::Output {
        self.number
    }
}

// When called with one argument of type `Operator`.
impl FnOnce<(Operation,)> for Number {
    type Output = isize;
    extern "rust-call" fn call_once(self, args: (Operation,)) -> Self::Output {
        // Do something with self and the Operation
    }
}

Recap

assert_eq!(seven(times(five())), 35);
                 ^^^^^ Not found in this scope

Congrats! We can now create a struct and treat it like a function with overloading. Way simpler than doing it in JavaScript/Python/Ruby! Of course I’m joking, but I still think Rust is super interesting. More dynamic languages will always win at code golf, but Rust is crazy fast, crazy safe, and is a very positive movement in programming languages.