In Rust, function signatures tell a story. Just from glancing at the signature of a function an experienced Rust user can tell much of the functions behaivor.
In this article we'll explore some signatures and talk about how to read them and extract information from them. While exploring, you can find many great function signature examples in the Rust API docs. You can play around on the Playpen.
This article assumes some knowledge of Rust, glossing over a bit of the book should be quite sufficient if you are lacking that but have programmed before.
If you're used to programming in something like Python or Javascript, this all may seem a bit foreign to you. I hope by the end of it that you're convinced this additional information is both a good thing, and that it is not something you often have in dynamically typed languages.
If you're used to C++, C, or the other systemsy languages hopefully this should all seem very familiar, despite the syntax differences. Ideally by the end of your article you'll think more about your function signatures as you write them!
Baby Steps
Your first function definition in Rust almost definitely looks like this:
So since you've already most likely wrote this let's start here!
fn
: is the syntax which tells Rust we're declaring a function.main
: is the name of the function.main
is special because it's what the program invokes when built and run as a binary. Function names are alwayssnake_case
and notcamelCase
.()
: Is the arguments list. In this case,main
accepts no arguments.{}
: Are the delimiters for the inside of a function. In this case, it's empty.
So what would we write for a function that does nothing useful?
Great, now you too can do nothing useful!
Visibility
By default, all functions are private and cannot be used outside of the module they are in. Making them usable by a different module is simple.
// Optional to avoid `foo::`
use public_function;
Like mutability, Rust is conservative in its assumptions about things like visibility. If you try to use a private function the compiler will let you know and help point you to where you need to make the function public.
If you have a function like foo::bar::baz::rad()
in your project and want to make it usable as foo::rad()
add pub use bar::baz::rad;
to your foo
module. This is called re-exporting.
Simple Parameters
No longer happy with do_nothing_useful()
you decide to adopt a dog. Good for you! Now you have a new problem, you have to walk it and play with it!
Parameters are declared variable_name: Type
, and are comma seperated. But c'mon! Our dog is a lot more than just a String
! Good news, you can use your own types too.
; // Let's not go overboard.
; // Simple types in demos!
Great, looking better already. Let's get that awesome day started.
Whoa whoa! That's a perfectly good day the compiler is totally ruining for us! Rover is going to be super sad.
Let's look at the error:
<anon>:11:15: 11:20 error: use of moved value: `rover`
<anon>:11 play_with(rover, fetch);
^~~~~
<anon>:9:14: 9:19 note: `rover` moved here because it has type `Dog`, which is non-copyable
<anon>:9 walk_dog(rover);
^~~~~
Here the compiler is telling us that rover
was moved when we passed it into walk_dog()
. That's because fn walk_dog(dog: Dog) {}
accepts a Dog
value and we haven't tell the compiler they are copyable! Values with Copy
are implictly copied when passed to functions. You can make something Copy
by adding #[derive(Copy)]
above the declaration.
We're going to keep Dog
not copyable because, gosh darnit, you can't copy dogs. So how do we fix this?
We could clone rover
. But our Dog
struct isn't Clone
either! Clone
means we can explicitly make a copy of an object. You can make something Clone
just like you did as Copy
. To clone our dog you can do rover.clone()
But really neither of those possible solutions solved the real problem: We want to walk and play with the same dog!
Borrowing
Can I borrow your dog?
Instead of moving our Dog
into the walk_dog()
function we really just want to lend the function our Dog
. When you walk your dog it (generally) ends up coming back to the house with you, right?
Rust uses &
to symbolize a borrow. Borrowing something tells the compiler that when the function is done the ownership of the value returns back to the caller.
There are immutable borrows as well as mutable borrows (&mut
). You can have an immutable borrow passed to any number of things at once, and a mutable borrow only passed to one thing at a time. This provides data safety.
So our new borrowing functions don't really cut it, do they? We can't even mutate the Dog
! Let's try anyways to see the error message.
As we expected:
<anon>:6:5: 6:22 error: cannot assign to immutable field `dog.walked`
<anon>:6 dog.walked = true;
^~~~~~~~~~~~~~~~~
error: aborting due to previous error
Changing the function signature to fn walk_dog(dog: &mut Dog) {}
and updating our main()
we can solve this.
As you can see, the function signature tells the programmer if a value is mutable and if the value is consumed or referenced.
Returning
Let's revisit exactly how we get Rover, because thats how we can explore return types! Let's say we want a function adopt_dog()
which takes a name and gives us a Dog
.
So the -> Dog
part of the function signature tells us that the function returns a Dog
. Note that the name
is moved in and given to the dog, not copied or cloned.
Inside Traits
If you're implementing functions in a trait you also have access the following two tools:
- The
Self
return type which represents the current type. - The
self
parameter which specifies the borrowing/moving/mutability of the structure instance. Inwalk()
below we take a mutable borrow, a bareself
moves the value.
An example:
// ... `Dog` struct from before.
Generics
Let's face it, there are a lot of different kinds of dogs! But moreso, there are a lot of types of animals! Some of these we might want to walk too, like our Bear
.
Generics let us do this. We can have a Dog
and Bear
struct that implement the Walk
trait, then have a walk_pet()
function accept any Walk
traited structure!
Generics are specified to functions in between the name and the parameters using sharp brackets. The important thing to note about generics is when you're accepting a generic you may only use the functions from the constraints. This means that if you pass a Read
to a function that wants Write
, it still can't Read
in it unless the constraints include it.
You can also use a different where
syntax as function signatures with complex generics can get rather long.
If you have multiple generics you can comma seperate them in both cases. If you'd like more than one trait contraint you can use where W: Walk + Read
or <W: Walk + Read>
.
Look at all of the information you can derive from that function signature! It's not helpfully named but you can still tell almost for sure what it does!
There are also these crazy things called Associated Types which are used in stuff like Iterator
. When being written in a signature you want to use something like Iterator<Item=Dog>
to say an iterator of Dog
s.
Passing Functions
Sometimes it's desirable to pass functions into other functions. In Rust, accepting a function as an argument is fairly straightforward. Functions have traits and they are passed like generics!
You should definitely use the
where
syntax here.
Functions in Rust implement traits which determine where (and how) they are passed:
FnOnce
- Takes a by-value reciever.FnMut
- Takes a mutable reciever.Fn
- Takes a immutable reciever.
A particular Stack Overflow answer summises the differences very well:
A closure
|...| ...
will automatically implement as many of those as it can.
- All closures implement
FnOnce
: a closure that can't be called once doesn't deserve the name. Note that if a closure only implementsFnOnce
, it can be called only once.- Closures that don't move out of their captures implement
FnMut
, allowing them to be called more than once (if there is unaliased access to the function object).- Closures that don't need unique/mutable access to their captures implement
Fn
, allowing them to be called essentially everywhere.
Essentially, the differences between the different types is how they interact with their environment. In my experience, you only really need to worry about the distinction for Closures, which may capture variables in scope (in our above example, the main()
function).
Have no fear, though! The compiler messages when one type is provided when another are needed are very helpful!
Lifetimes
So, you're probably feeling pretty good about yourself right now. I mean, look at that scrollbar, it's almost to the bottom of the page! You'll be a Rust function signature master in no time!
Let's finish up with a bit of talk about lifetimes because you'll eventually come across them and likely get quite confused.
Let me be honest with you upfront here. Lifetimes are an arcane art to me. I used them a bit back in 0.7-0.10 and then I haven't really had to use them since. If you know really anything at all about them you're much more qualified to write this section than I am.
Modern Rust has a really robust and effective lifetime ellision which removes the vast majority of lifetime gymnastics we used to need to concern ourselves with. But when you do things can start to untangle.
So, if you start dealing with a lot of lifetimes, your first step should really be to sit back and think about it. Unless your code is quite complex it's quite likely you won't need to deal with lifetimes. If you're bumping into lifetimes in a simple example your notion of the problem is probably incorrect.
Here is a function with lifetimes from Option
's implementation.
Lifetimes are denoted by the tick ('
) and given a name. In this case, 'a
but they can also be things like 'burrito
if you prefer inside jokes. Essentially what this is saying is:
The lifetime of the
Option<T>
this is called upon is the same as the lifetime of the returned[T]
Great! I'm really not qualified to write anymore about lifetimes but if you have anything to add let me know and I'll credit you for sure.
Challenge Time
Below, you'll find a set of functions pulled from the standard library along with links to their documentation. Can you tell from their function signature what they do? (For added fun, I've removed the function name!)
// In `File`
// In `Option<T>`
// In `Iterator<Item=T>`
// In `Iterator<Item=T>`
// In `Result<T,E>`
I hope that went fantastically, I was just over here cheering you on!