Understanding Rust Closures aka. Anonymous Functions 🦀 💻

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
hey guys my name is Trevor Sullivan and welcome back to my video channel thanks so much for joining me for yet another video in my rust programming series now if you're following along with my rust programming playlist then you probably know that our last video talked about control flow statements including things like for Loops while loops and so on and so forth before that though we actually had another video that talks about functions as well as modules that allows you to separate your code into different logical components now we're going to be building on top of functions but in the functions video that we did previously we only discussed named functions where we actually use the FN keyword here to specify a function with a given name any input arguments any return type that the function returns back to the color and things like that pretty simple stuff as far as most programming languages go now one of the interesting things about rust is that rust supports like many other languages a concept of an anonymous function in Rust we refer to these Anonymous functions as closures although you will see references to the term Anonymous function inside of the rust documentation so if you're interested in following along with the rusts documentation you can head over to the rust programming language book online and if you do a search here for the topic of closure you can go to this article right here called functional language features iterate iterators and closures that's kind of the top level category but then that section 13 right here actually breaks down into some subtopics and the section 13.1 specifically covers closures inside of rust now the interesting thing about rust closures is that they work a little bit differently than Anonymous functions or what are sometimes known as lambdas in other languages so we're going to be exploring exactly how these closures work so that by the end of this video you should have a pretty solid understanding of how lambdas work or closures work rather in Rust and you'll be able to apply that knowledge to your own rust programs now for starters what we're going to do is switch over to our vs code editor here and to segment my code logically I'm actually going to separate the code for closures into a separate module so the first thing that I'm going to do is just create a new file in my rust project here and we'll just call this closures dot RS for now and then inside of here we'll go ahead and Define a function called test closures and inside of this function we'll explore the concept of closures so that we can kind of separate everything into this dedicated function I'm also going to expose that as a public function so that I can call this module from my main function so let's head back over to main.rs right here and we want to import that closures module so do Pub mod closures you can see it's Auto completing for us because it detects the different dot RS source code files in our project and then down at the bottom here we'll just comment out the test 4 call and then we'll call out to the test closures function all right so back over in closures.rs it's time for us to take a look at the magical syntax of rust closures now if you're new to programming or even if you're coming from another language the Syntax for rust closures can be a little bit confusing because there's actually some ambiguity with some of the characters the special characters that are used in the syntax to define a closure let's start by looking at the most simplistic example of a closure and then we'll get a little bit more advanced as time goes on so that we're not biting off more than we can chew at a single point in time so the first thing that I'm going to do is Define a simple closure and all it's going to do is just print out some text so this would be equivalent to basically just writing a function and then calling the print line macro as we've been doing in most of my videos and we'll just say returning some text right but we don't want to do this just inside of the test closures function we want to actually create a closure itself so how do we declare a closure well a closure being an anonymous function can be assigned to a variable just like other core data types in the rust language so the first thing that we typically do when defining a closure is use the let keyword to declare a variable and let's just declare this as Let's do an add function for now and we're going to add two numbers eventually but for now we're not going to take any input parameters and then we're going to set this variable equal to two pipe characters directly following each other and then just a space followed by the print line macro call now this might look pretty confusing and if it looks confusing I don't blame you it it is kind of confusing the first time that you look at it but once we break this down it actually makes a lot of sense now if we compare a closure in Rust to a standard function we can see that there are some differences first of all you'll notice that we don't have a name for the function because the function's name is essentially just the variable that it's been assigned to so that's ultimately how we're going to call the closure call the anonymous function is by using the variable name that the function has been assigned to something else that you're going to notice is that we are missing the open and close parentheses that we typically see following a function definition now the double pipe characters that you see here actually replace these parentheses so anytime that you see these double pipe characters and it's not part of a comparison expression like a logical or operation where it's you know true or false or X is greater than 5 or X is less than 10 you know some kind of comparison like that if you see this as part of a standard statement here there's a good chance that this is actually going to be a closure in Rust code so keep your eyes peeled out if you're scanning through code for someone else's project for this type of syntax even though it's kind of hard to pick up on at first that is how a closure is defined now looking past the input arguments or parameters to the function you're also going to notice that there is an absence of the curly brace characters surrounding the body of the closure with closures you don't have to specify the curly braces although they are optional if you do want to use them in a multi-line function then you can specify them however for syntactical brevity the rust development team has chosen to leave the curly braces as optional if you just have a simple one line closure such as the print line statement that we have right here so at this point we've declared a simple closure and you'll see that our compiler is inferring that the implementation of a trait is actually based on the FN trait now there are three different types of traits or three different traits specifically that are available for closures and that all depends on exactly what the closure is doing so if you have a closure that's mutating an object that it doesn't own then you actually see the FN mute trait being implemented by that and then there's a third one that you can refer to in the documentation as well but for now you'll just see that we have unused variables so we have this variable called add and it's pointing to a closure and we are not using it so let's go ahead and use it by simply calling add here if we do Ctrl s to save our closures.rs source file you can see that the compiler is no longer complaining that we are not using this add variable here let's go ahead and actually try to run the code though so we'll come back to our terminal and do a cargo run and sure enough right down here you can see it says returning some text and that's coming from this print line macro call right here so even though the print line statement isn't part of a function directly it is part of a closure and we called that closure by using the standard parentheses that we would use to invoke any other function right let's take things one step further with this closure though so far we have a simple closure that just has a single one-line statement but it doesn't accept any input parameters right and so that's what goes inside of these double pipe characters right here very similar to a named function that uses the parentheses instead so let's declare an input argument like maybe X as an integer 8-bit so that'll be a signed integer that can be either positive or negative and as you can see we're now getting this complaint here from the compiler saying that hey the add function here which is an anonymous function expects there to be one argument which is X and we haven't passed any in so we can go ahead and just pass in maybe a number like five or negative three or something like that and that will satisfy assigned 8-bit integer right if we try to do something big like negative 1000 then that's going to be out of the range of an 8-bit integer but you know we could just bump it up to something else like a 16-bit integer 32-bit 64-bit or 128 bit integer value all right so let's bring this back down to an 8-bit and then we'll do negative three here and we can run this but you're going to see that we have an unused variable within the scope of the closure itself because we're doing print line but we're not specifying X anywhere so let's go ahead and actually plug that in right here so we'll put a placeholder with curly braces and then we'll pass in x all right so now we're using it the compiler stops giving us that warning and now if we do a cargo run you can see it says returning some text negative three so we just pass that integer into our string template and it prints it out now we can specify multiple input arguments just like any other function right so we could do maybe Y is equal to maybe an i16 and then we could say put another placeholder here pass in y now of course it expects there to be two input arguments so we'll pass in the number eight as well and very similar to a regular function it prints out both X and Y even though they're different data types now here's what's interesting about closures this is unique compared to standard named functions in this test closures function here if I wanted to declare an input like double X for example you're going to see that we immediately get an error from the compiler saying that there's missing type for function parameter and that's because when you declare a named function we have to specify the data type of that input parameter so let's do something like an i16 and that satisfies the compiler error for declaring the data type but watch what happens with closures closures are very unique because we don't actually have to specify the data type of a input parameter or a closure as soon as I removed the data type declaration from the input parameter X you can see that instead of an INT 8 it actually changed to an i32 instead because the rust compiler was able to infer the data type that I was passing in when I actually invoke or call the closure right so this is something that's very interesting about the rust compiler is it's actually looking ahead in my code at the next line and saying hey I see that you're passing in an integer value so I'll go ahead and just assign an in32 to that and that should satisfy its needs here now we can also specify a data type in addition to inferring other data types so X is currently being inferred because we didn't specify it that's just the editor kind of displaying to you what is being inferred right there but y still has i16 assigned to it as the data type but I could go ahead and remove that as well and now you can see that both X and Y are both being inferred as a 32-bit signed integer and you can also see that in the function signature right here that's being again inferred there is no return type because we aren't returning anything from our closure we also don't have to declare a return type so what we can do is maybe just say let's add these two numbers together we'll just say X Plus Y and save that and now you can see that this function implementation that has been inferred now is showing that we are returning a 32-bit signed integer because X and Y are both sine 32-bit integers we're doing a simple mathematical operation on both of those input parameters so the result must be a 32-bit integer but let's say that we had a couple of different data types let's say that we declared y as an unsigned 8-bit integer okay well let's pass in both positive values since we're dealing with unsigned integers now and what you'll see is that X automatically updates to an unsigned 8-bit integer because three works as an unsigned 8-bit integer and so does the number eight and so the inference here automatically updates based on a look at all of the parameters that we're passing into the function and rust is trying to make the function work however if I try to pass in something else like maybe a string value let's just do a static string like Trevor here well passing in an integer with a static string doesn't really make any sense so rust isn't able to do the inference of this operation here so we can't just do integer plus a static string right so we would have to declare exactly what we want rust to pass in here so in the case of Y we could say I want that to be a string point enter value but of course now we're going to get an error from the compiler saying that we can't add the integer and the string together so we'd have to either change the implementation of our function body our closure body specifically or we would have to change the input arguments that we're passing in here so that's the interesting thing about closures is that the input argument data types can actually be inferred by the rust compiler so that's just like a little value add feature that we get from rust now closures don't have to be just one line as we mentioned before if I wanted to take this existing closure and turn it into a multi-line function I can simply surround our function body here with these curly braces and that will allow me to add additional lines of code just like we do with named functions so in this case let's just remove the semicolon there so that we can return the value of x plus Y and then you can see right here that this function signature works perfectly fine we could also do kind of a debug statement here and say x is this and Y is this so we can just print out the value of both of these so that if we run into any problems with our code we can kind of figure out what's going on also I forgot I plugged that input argument there so now we're printing out three and five but we're also returning the value from this function invocation but at the moment we're not actually capturing the results from this function invocation so even though the rust compiler has inferred that we are returning a 32-bit signed integer we're not actually using that value anywhere back in the parent function test closures so let's say let results equal the result of the add function and once again you can see that this result variable is being inferred as i32 because rust knows that our closure is returning an i32 all right so that's really nice to be able to do multi-line functions here with closures but there's something else that's very interesting about closures which is that they actually inherit the variables that are defined in their parent scope so if we take a look at our test closures function right here we can see if we just look for the let keyword here we have two different variables defined we have the add variable and then we also have the result variable so let's say that we wanted to take this result and do something with it inside of a different closure well of course we would be able to do that because result has been defined in the closures parent scope so let's go ahead and take a look at how this works we'll create another closure here called print result and we'll set this equal to a couple of pipe characters for our input arguments I'm not going to use any input arguments here but I'm just going to say print line and we'll say the result is we'll put a couple of curly braces there and then we're just going to directly pass in the result variable and this is totally valid so even though the closure here itself doesn't declare the result variable the result variable is bound to the closure simply because it was declared in the parent block of where the closure itself was declared so if we were to actually execute this closure right here called print results then we should see that the result is eight of course our inputs to the add function were three and five the result got assigned to the result variable which is an i-32 and then we directly grabbed that result from the parent scope without even having to declare any input arguments and you can mix and match that as well so we could Define input arguments and we could then use those input arguments in conjunction with the variables that were inherited by the closure from the parent scope right so if we just said you know X is an i32 then we could do something like the results is results plus X and then we could pass in a number like 93 or something and then if we run this you can see that we get the result of 8 from the first execution of the add closure here and then that gets assigned to result then we do result plus 93 so 8 plus 93 is 101. so it's really interesting because you don't have to declare input arguments you can access those variables directly from the parent scope but in some cases you will want to be explicit and declare those input arguments here so just be aware of that inheritance with closures now I think the last topic that I wanted to cover for now as far as closures go is how you can mutate objects inside of closures so if we take a look at this example right here with the print results closure you can see that I accessed this result variable that we declared right up here as a result from the add function right we took that i-32 out of the add closure assign it to this result variable which is an i-32 and then we just access it we read that value inside of the closure called print result however we're not mutating the value of anything in here right from the parent scope all we're doing is we're going to the parent scope we're reading the value and that's all that's happening but what if we want to borrow this value and then mutate it inside of the closure itself well how can we accomplish that you might think well we could just do you know let's make result mutable right well we're actually going to take a look at a different example here of how this works and it's going to be a little bit bizarre but once you understand how it works hopefully it'll stick in your mind and then you'll be able to use this technique in the future so in this particular example I'm going to create a custom struct and I realize that we haven't covered custom structs yet but don't worry too much about it I'm going to cover them in a future video but I'm just going to define a struct called person here and the person's struct is going to have two different properties it's going to have a first name property of type type Capital string and then we'll do last name of type Capital string as well and then we're going to instantiate a new person set the first name and last name values we'll initialize those but then we're going to use a closure to actually mutate that person's struct instance inside of the closure rather than inside of the function where the struct instance was instantiated so right down here in our test closures function we'll just say let P1 equal we'll do person say first name is Trevor and then we'll do last name is Sullivan put a semicolon there at the end and the reason we're getting these Red squigglies is because we need to take that and call tostring to turn it from a static string into a string that's allocated on the Heap and then we also need to call tostring here as well that's the capital S string type not the pointer to a static string so now we've got this P1 variable declared and we want to build a closure that changes either the first name or the last name or both doesn't really matter and we want to mutate this object but we want to mutate the object that's in the parent scope not defined or declared and initialized in the closure scope so let's do a new closure I'll call it change name and we're going to set this equal to a couple of pipe characters put a semicolon at the end and then we need to specify what we're going to mutate here so let's do P1 Dot and then we'll say last name equals Jones and then we'll do Jones dot tostring because the data type of first name and last name is a capital S string so we'll go ahead and save that and we get one warning here saying that our change name variable is never accessed because we're not actually calling this new closure that we created but take a look at what's happening right here we are actually getting a compiler error here because it says that P1 dot last name is not declared as mutable so you might be thinking well I'm just going to come up to the line where we declare and initialize the P1 variable and say let mute that should take care of it right well let's give it a try and see what we get if we do a cargo run let's actually print out the value as well so we'll say print line and say let's just do actually a curly brace and then P1 dot last name that we can actually see what the last name is is it the original or is it the new value right so as you can see right here the result is still Sullivan it hasn't actually been changed to Jones and that's because we never actually called the function so let's go ahead and right before that we'll say change name and we don't take any input arguments here but this is the error that I was actually expecting to receive right here see how we get this error that says cannot borrow change name as mutable as it is not declared mutable so this is actually referring to the closure itself it's not necessarily referring to P1 because P1 has been declared as mutable in the scope where it's declared and when we try to access a mutable variable and actually modify the value of that mutable variable we have to also declare the mute keyword on the closure itself so this is very different from when we were just reading a value from the parent scope like this right here where we do print result and we access the results variable but this is a read-only operation we're not mutating results in this example in this example though we are mutating the P1 object right here which is of type person the person struct we're are mutating the last name field of that struct and therefore we have to mark the closure as mutable as well so now let's go ahead and do another cargo run and now you can see that the closure has successfully modified the object that was declared and initialized in the parent scope E1 of type person right so this is something that you really need to understand is not only does the variable itself need to be marked as mutable but the closure that's actually doing the object mutation also must be marked as mutable and then something that might be useful here rather than just taking a hard hard-coded value here we could actually declare that as maybe input that we could then pass in from the function call so we could say new last name for example and we'll mark it as a string pointer and then we could pass in Jonesy or something like that or Joan Jonesy maybe something like that I don't know but then we'll set this to new last name instead of just hard coding Jones there and if we run this again now we have Jonesy which is being passed in as an input argument great so we're just kind of building on top of our understanding of closures here but there's another thing that I want to show you here the whole concept of borrowing that we just discussed where this closure is borrowing access to P1 so that the closure itself can mutate it this borrowing actually happens as long as the compiler knows that we are going to be accessing this variable from inside the closure so let's just kind of Step through what's happening here first we instantiate the person we initialize that value next we declare the closure but we don't call it next we call the closure and then we print out the last name but Watch What Happens let's say that we duplicate this line where we invoke the closure and let's change it to a different name like maybe O Sullivan okay guess what happens well once again we get this error message that says cannot borrow p1. last name as immutable because it's still borrowed right so again what's happening here just like earlier the compiler is actually looking ahead at our code and it can see that we are borrowing P1 not just once but actually twice here so each time that we call change name we still need to have P1 borrowed so that the closure has permission to update that field in our person's struct so therefore we will not be able to access P1 last name until after we have completely finished accessing p1. last name from our closure so after we call change name two different times then we can go ahead and start to read that value again from the parent scope and sure enough you can see we change it to Jonesy we change it to O'Sullivan and so the final result is O'Sullivan so anyway I think that's pretty much everything that I wanted to cover in this video about closures I know they can be a little bit confusing the syntax is a little bit weird it's a little bit bizarre when you get into things like mutable values that you're using inside of a closure but once you kind of play around with things Hands-On I always encourage people spend Hands-On time with rust don't just watch this video go ahead and Implement these Concepts in your own program don't just copy the code I have here come up with your own Concepts come up with your own examples maybe use temperature data as an example like a weather forecasting app or maybe an employee database type of application just come up with some other kind of example where you can use these Concepts but customize it enough that you're actually understanding these Concepts rather than just copying what you see right here on the screen anyway thanks for joining me for yet another video make sure to check out the rust playlist over on my YouTube channel here I've also got some other playlists pinned right here like windmill.dev low code lxd virtualization open source software in general Powershell automation Cloud stuff and I'm going to be coming out with more videos so stick around subscribe like this video comment down below and let me know what you thought thank you so much for watching and we'll see you in the next video take care
Info
Channel: Trevor Sullivan
Views: 11,260
Rating: undefined out of 5
Keywords: rust, rustlang, rust developer, rust programming, rust software, software, open source software, systems programming, data structures, rust structs, rust enums, rust coding, rust development, rustlang tutorial, rust videos, rust programming tutorial, getting started with rust, beginner with rust programming, rust concepts
Id: qXNUHfpalts
Channel Id: undefined
Length: 30min 22sec (1822 seconds)
Published: Sun Aug 20 2023
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.