hey YouTube in this one I'm going to talk about functions specifically how rust represents functions in the type system we're going to talk about a mental model that helps make sense of some of rust design choices that might seem strange at first glance and then stick around because we're then going to talk about some major practical implications of these design choices on your everyday code featuring a cautionary tale from another programming language that made some different design choices from rust now to be clear this video is not going to be about rust's three function traits
these are very important to know about and understand thoroughly if you want to write effective rust code but they're not the topic of this video in this video I'm going to talk about the actual types of function items like Foo here and I'll also talk about closures too so let's get started so we're going to start by learning a bit about function types through some experimentation so first let's meet our test subjects the first is a function called f that takes a 32-bit integer and Returns the result of adding that integer to itself the second
is a function called G that also takes a 32-bit integer and Returns the result of raising it to the fourth power notice that these two functions have the same signature they both take a 32-bit integer and return a 32-bit integer but the bodies are different so those are our test subjects now let's meet some tools we'll be using for our experimentation sort of our lab equipment so for the next few pieces of code I'm doing a glob import of the stood any module the stood any module has lots of interesting tools for dynamically examining the
types of values and is perfect for this kind of experimentation although I should mention that you should probably be careful using it in real code the tools inside are very powerful for getting you out of a jam but they're probably not good things to Anchor Your Design around anyway I'll be using them here I'm also using this function type name of vowel which takes in a reference to a value and Returns the name of its type as a string this function is actually provided by the standard library in the stood any module but as of
the day I'm recording this it's unstable so here's a stable implementation if you need it all right so let's get experimenting so here are f and g and you remember they both have the same signature and if you're coming at this fresh especially if you're coming from another language you might reasonably assume that because they have the same signature they also have the same type so let's test that assumption I'm going to assert that the type ID of f is equal to the type ID of G so if we run this code we'll see that
this assert actually fails so we can see here that the type ID of f is not equal to the type ID of G so because F and G's type IDs are different we can conclude that they actually don't have the same type so let's fix this assert so if f and g have different types what are they what's the type of f well let's try printing it out so when I call type name of Val with f and print it out I get this the type name of f is f and by the way I'm
in a module called example so that's why it's example double colon f anyway so this is strange so far and if we change it to G we see that the type name of G is G so what we've learned so far is that f and g have different types and the types seem to just be themselves so let's really prove this to ourselves if f and g are different types that should mean that if I stick F in a local variable and then later try to assign that local variable to G I should get a
compiler error and that's exactly what happens you see that it's saying that local is of type the function that is f and yet we're trying to assign it to a value of type the function that is g now interestingly the names of the types that the compiler is telling us right here are different than what we just saw from type name of foul and the reason for that is that these types actually don't have a name they're types that exist internally inside the compiler's understanding of our code but they don't have a canonical representation in
text so we couldn't do like let mute local colon and then spell out the type of f after the colon because F doesn't have a type that we can actually spell in our source code and so the compiler versus the standard Library are rendering that type in two different ways but it is the same type that they're talking it's this unknowable unspellable type of f and g respectively and if we look further down you can see that the compiler is just telling us exactly what's going on different function items have unique types even if their
signatures are the same now this is an excellent error message and in typical rust C fashion it's teaching us rust instead of just yelling at us but if I were going to tweak it I would add that it's not just signatures being the same every single function has a different type if I introduced another function f Prime that has the exact same signature and the exact same implementation as F and then I tried to use that in this situation I would get the exact same error the compiler wanted F and I gave it f Prime
so all functions have different types so how do we get this to work I mean it's useful to be able to store functions in local variables kind of dynamically well the way that you get it to work is by explicitly introducing indirection by annotating local as a function pointer type now F coerces into a function pointer type when we assign it to local and then G does the same on the next line also note the lowercase FN here this is the syntax for spelling out the type of a function pointer whereas the capitalized FN is
the name of a trait so make sure you know the difference between the two next I want to make sure I mentioned that the exact same situation holds for closures as well so you see here that I'm assigning a closure into a variable called local and then on the very next line I'm assigning the same closure into it again but the compiler tells me that this doesn't work it tells me again that no two closures even if identical have the same type now the fix for this error is the same you can annotate local as
having a function pointer type and then the closures will implicitly coerce into function pointers when you assign them into local but beware that this only works for closures that don't capture anything in their surrounding environment so you can see here that I have a closure that captures the value of y That's declared right above it and in this case I get another error that tells me that closures can only be coerced into function pointer types if they do not capture any variables and by the way this makes sense when you think about what you're asking
for Here Local is just a function pointer it's just the address of some instructions to execute but on this line you're trying to store inside of local not just the address of some instructions to execute but also the value of y and local is just a pointer it has no place to store y for you so this doesn't work and if you do want a way to abstract over closures that may or may not have captures you need something more powerful you need a trait object but that's a topic for another video so what's going
on here so we just learned that all functions in Rust have different types so how do we make sense of that and what does it mean not what do we gain we'll talk about that soon but how does it fit into our mental model of type systems and data in our program well I think the following illustration is pretty interesting and helpful warning this next section contains some original thought and it's possible that not everything I'm going to say is going to be true down to the letter of the law in Rust but regardless I
think it's valuable food for thought so here are our old friends f and g as they exist in our rust code but before these functions run at runtime they undergo a very important transformation the compiler takes them and transforms them into machine code that has equivalent Behavior to our rust code and these machine code instructions end up in the final binary of our program and they get loaded into memory when our program runs at which point their data living in memory just like all of our other data well not just like all of our other
data because they're executable code but they're still made of zeros and they're made of bytes and they live in memory and they take up space but we use types to describe the shape of data that lives in memory but if these functions and all of the bytes that make up their instructions are just bytes that live in memory then we have to ask ourselves what is the type of those bytes and well the type of the bytes that make up that top function f and those bytes at the bottom combine to form a value of
type G but if you've been paying close attention you might realize that this isn't quite right you remember that functions actually have types that are not nameable so it's more like the bytes in that top function are of type something and the bytes in that bottom function are of type something else and then f and g are like Global constants that are the one and only values of those types in our program and so what can we say about these two unnamable types well they're different types that's for sure they're also zero sized which I
haven't mentioned yet they don't actually contain the instructions for your function in a physical way they just kind of name them so they're zero sized and in that sense they only exist at compile time and otherwise they're just these kind of opaque unknowable things that we can't really get at except through the traits that they Implement including the FN traits if you squint you can kind of think of the FN traits as the ABI for calling a function we don't know exactly what's inside the function but we can call it as long as we know
how to set up everything according to the calling convention and personally I think this is a really really elegant design for function types it's not like in other languages where functions are just this magical thing you can call because you just can because they're a language feature and then maybe there's some other abstraction for implementing your own callable things in Rust functions are just these weird values of opaque type that you can call because they implement the FN traits I think that's pretty cool and it feels like a nice Grand unified theory of function calling
that other languages don't execute as cleanly but like what's the point are we just here to geek out about language design well I've been promising you a practical application and it's time so in order to understand the Practical benefits of this design first we're going to look at another language that does things differently in a way that leads to problems and that language is C plus now to give C plus plus a little bit of credit it inherited a lot of the decisions I'm going to talk about here from C and in many ways when
C plus plus came along it did the best that it could but it still ended up in a situation with some unfortunate problems so let's take a look so really quick here are two functions f and g translated into C plus and right off the bat we can see that f and g have the same type so the static assert succeeds the decal type of f is the same as the decal type of G and so what is that type well it's kind of a mouthful to get the name of a type at runtime in
C plus plus but you can see here that I'm just printing out the name of the type of F and the name of the type of f is this weird int int thing and if you're not as used to C plus plus syntax the second int here is the parameter of type int and then the first one is the return type int and so you can see that the type of f doesn't refer to F itself in any way it's just this kind of abstract representation of the signature of F and so it makes sense
that the type of f and g are the same they're both just this weird intent thing and so how does this become a problem well it becomes a problem when you start talking about higher order functions in particular functions that take other functions as arguments so imagine a function like this in Rust that takes in a function as a parameter and the function has to have the same signature that we've been talking about for the whole video i32 to i32 and you see that in the body we just call it five times with the number
zero one two three four and sum up the answers so if we take this function and translate it into C plus plus here's what it looks like in C plus plus 20 this can cause problems particularly performance problems when we pass around functions naively in C plus and in order to see it in action I'm going to ask you to come with me to my favorite website compiler Explorer so here we are on compiler Explorer and if you don't know about compiler Explorer do yourself a huge favor and check out compiler Explorer there will be
a link in the description but long story short we have some source code on the left and you can pick from any of these languages that it supports and on the right we have the result of running the compiler on that source code so I have some C plus plus code on the left and I'm running it through the clang compiler and right here I have the x64 assembly that clang generated and you can see that we don't have much going on right now because the C plus program does literally nothing in its main function
but you can see that just above main I've copied over the functions that we've been talking about I have F right here and then I have our calculate higher order function here you can see that I've added this attribute no inline and that just makes sure that the compiler doesn't inline the calculate function itself just so that we can see what's going on a little bit better in a moment and so down in main I want to call calculate with f and remember that calculate takes the number zero one two three four calls our function
with each of them and then adds up the results so we should get 20 here because F based basically just doubles its input so we're basically just adding up the number zero two four six and eight so let's try it out let's call calculate with f and so look what happens here we actually get the right answer because the main function returned 20 but let's look at what happened in the generated code so right off the bat we can see that the function template calculate that we wrote was instantiated with a parameter of function pointer
type you see this type int star int and by the way you might be curious where the pointer comes from because we didn't take the address of anything in our code C plus plus has a quirk where when you pass a function by value to another function it undergoes a process called Decay where it just kind of implicitly becomes a pointer so that's what's going on there but already this should raise some suspicion the C plus plus compiler had to stamp out an instantiation of calculate that works for any function pointer not just for f
because the type of f doesn't say anything about F itself it only describes the signature of f so when we pass it to calculate the compiler substitutes in the types and then it has to generate code that works for any function with that signature and as a result you can see that inside of the body there are these call instructions where the operand is this register RBX and what that means is that we are calling into some unknown code whose address lives in the register RBX because that's where our function pointer parameter ended up and
this is called an indirect call because we're calling into code that we don't know what it does or even where it is ahead of time and this prevents your CPU from doing clever things like instruction prefetching but perhaps more importantly it prevents the compiler from being able to see inside the function that it's calling so that it can do optimizations like inlining so that it can collapse the body of calculate into something much simpler I mean think about it we're passing in a specific function by name to calculate but because of C plus Plus's type
system the compiler has to generate code that's as if it has no idea what function we're passing in and notice also that the compiler has to generate code for the function f even though in this example I've marked f as static which among other things tells the compiler that F doesn't really have to appear in the final binary and it's okay if it's optimized out completely but it can't be optimized out completely because we need to be able to take its address to pass it into calculate here and this all stems from the fact that
in C plus function types don't contain any information about what function it is they only know what signature it is and so when you pass them around you have no choice but to call them indirectly and so the typical workaround for this problem in C plus plus is to wrap the function you want to call in a Lambda expression Lambda in C plus plus behave much more like function items in Rust where each and every Lambda expression has a different type and look at what happens our calculate template is now being instantiated with this Lambda
that lives inside the main function this happens to be clang's internal notation for representing lambdas but just like in Rust lambdas and C plus plus have an unspellable type but you can see that we're instantiating calculate with a Lambda type now and a Lambda type knows exactly what code it represents and so now the compiler is able to see straight through this function that we're passing in and you see that it has optimized the function body away completely and it calculates the answer at compile time and just returns 20. without having to do any work
at runtime besides Just Produce that return value and so this is what we want so this is a very common optimization technique in C plus where even if you just want to pass a function somewhere verbatim you still have to wrap it in a Lambda that just forwards to that function so that you get the code gen that you want and this is a very common situation in C plus where the shorter easier more beautiful way of writing something is actually worse for subtle technical reasons and you have to learn all these little tricks that
make your code a little bit uglier and a little bit less natural to read and write just to make it go faster so that's the situation in C plus and I'm sure you can already see where this is going but let's go look at the situation in Rust and see how it Compares and don't worry there will be a surprise or two so here's the exact same code in Rust so I have my calculate function that is inline never just so that calculate itself does not get inline so we can see what's going on and
then I have our old friend F right here and so down in the body of this function main 0 I'm going to pass F into calculate and immediately you can see over in our generated code that passing the function name into calculate is enough because F has a unique type that unambiguously identifies the code inside the function f here and so when calculate is monomorphized with f as a parameter the compiler knows exactly what code we're talking about and it can optimize away the loop and all the gross stuff in the function completely just based
on the function name now if we wanted to we could wrap this in a closure like we did in the C plus plus version but you see that the result is the exact same in Rust the simple beautiful thing is good enough already now the last thing I want to mention before we go is that I've been spinning this as a good thing because I think it generally is a good thing that named functions all trigger their own special monomorphizations but there's no such thing as a free lunch and I also need to mention that
having many many monomorphizations can bloat your program and be bad for compile times and bad for your final binary size which can have performance problems of its own but I want to show you one more trick that Russ C has up its sleeve to help you out here you remember F Prime from earlier it has the same body as F I've added it here below F and I've changed the body of main 0 to pass it into calculate instead of f so look over at our generated code we have a function called calculate we have
a function called f and we have a function called main zero now what happened to F Prime well the compiler has recognized that F and F Prime are exactly the same so it is decided that it only needs to generate code for f and f Prime will just be a synonym for f even though I'm actually using F Prime it's recognizing that F Prime and F are the same and it's just changing my calls to F Prime to be calls to F instead and to prove that that's happening let me change the body of f
Prime so it is different so if I add one more X here now I do get code generated for f Prime alongside F and if I take that X away again it goes away again but overall this is a nice little counter measure that the compiler does for you that might help in the fight against many monomorphizations of a higher order function leading to unnecessary code bloat and I recognize that it's not that common to write functions with identical bodies but keep in mind that this little trick kicks in after optimization so if I change
the body of f Prime to be x times 2 instead of X Plus X this ultimately boils down to the same machine code even though it's different rust code and you see that I still don't have F Prime over in my output on the right because the compiler looks at functions after they're generated to see if they're identical so as always the rust language and the compiler have your back and I hope that in this video I helped convince you that the way that rust models functions in its type system is not only elegant and
beautiful but also helps you out in your everyday code on a more General note I just want to say how incredibly grateful I am for all the support that these videos have been getting if you have any ideas for stuff that you'd want to do a deep dive on let me know otherwise I'll meet you in the comments section on this video and I can't wait to see you in the next one bye for now