Crust of Rust: functions, closures, and their traits

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments

nom nom nom nom

👍︎︎ 10 👤︎︎ u/zynaxsoft 📅︎︎ Oct 08 2021 🗫︎ replies

What a coincidence! I was just thinking of how long it's been since last stream.

Btw Jon, can you do a stream on bitwise operators?

👍︎︎ 3 👤︎︎ u/riasthebestgirl 📅︎︎ Oct 09 2021 🗫︎ replies
Captions
hello everyone and welcome back yet again to another crust of rust um this time i i wanted to tackle um functions and closures and the function traits and function pointers because sort of like the situation is with strings in rust there are a lot of different function things or function like things and the distinction is not always clear and i feel like it would be useful to just sort of get a survey of the scene um what we're going to talk about today is like um not necessarily a super large area we might be able to cover it in as little as an hour i don't know um but but i do think it's useful to have some information about the distinction between these different types and traits and how they interact just because you're going to come across them a lot like especially things like being generic over functions is um i think something you run into a lot in the rust world where you want to sort of have these nice um like callback functions or you want to be able to write things in a slightly functional style and then function pointers just come up everywhere and so i wanted to to make sure that everyone sort of understands that fairly um versatile primitive in rust before i start uh i do want to say that i wrote a book it is uh called russ for stations it is the it's intended to be basically a follow-up to the original west book the russ programming language by um stephen carroll so this book sort of picks up where that book left off and tries to give an introduction to all of the slightly more intermediate slightly more niche slightly more i don't quite want to say advanced although maybe that is an appropriate word too it tries to cover more of what you run into after you know russ the language and you want to know how to use it better for particular use cases and it focuses on things like idiomatic usage but also like how some of the more complex building blocks of the language work and how you can use them effectively the ebook version is now up on no starch press and the print book i is like with the printers and i think the goal is like early november for it to actually get printed and shipped from those starch um it will be get like distributed by i think penguin random house and because it's like global shipping problems it might take a little bit longer to get the book um if you buy through other retailers but it will be available on like amazon and stuff already as well um and yeah it's a lot of what i talk about here is covered in that book too uh please get the book it's a good way to support me as well if um if that's your your cup of tea um all right now let's get into functions and closures and traits so when we talk about functions in rust uh generally what people think about in fact let's let's start a new lib over here or something uh let's start a bin and we're going to call it um we're going to call it closure see it doesn't work i was thinking of closure in the sense of like uh feeling like you get closure in something but it has the same as the type so it doesn't really work let's go with um uh call me and what we're gonna do in this is well here we have a perfect example right so fn main is a function um perhaps unsurprisingly um and fn main specifically has a type like if if i in let's say i uh go bar over here so if i in main set let equal let x equals bar okay x now has a type what is that type some of you might say well it's a function pointer it is in fact not in rust in fact if we try to look at the type of this it sort of looks like it's a function pointer but it's not actually what this is is a function item uh which is subtly different notice here i'm not calling bar i'm just taking the sort of identifier of bar and the type of x here is a function item which is a zero sized value that is only carried around at compile time that references the unique function bar and in fact if if bar was generic then i wouldn't be allowed to do this because this does not uniquely identify a function i would have to do something like i32 for example so this now specifically names bar instantiated with the type i32 as t but if i now said uh x equals bar u32 then you'll notice here i get a mismatch type error and that is because this is not a function pointer right the the signature of this function right is that it takes no arguments and return nothing so bar i 32 and bar u32 are really the same in terms of if you thought of them as a function pointer but that's not what x is here x here is a function item that's uniquely identifies the function bar that takes i32 as a t if you try to assign that bar that takes u32 as a t they're not the same type and if we try to if i if i skip this and i try to do something like print line mem size of x ah size of i forget what it's called uh this is the size of vowel size of value and cd call me and cargo run you'll see that the size of this is zero zero bytes so it doesn't hold a pointer at all because this is just a an identifier that the compiler uses to identify this unique instance of this function now function items have a coercion defined for them into function pointers though is i can define a function baz that takes a function pointer which is written like this let's um i'm going to make this a little just give it a better signature and then change bar to match so bass here takes a function pointer that's what this signifies so here i'm saying i take a function pointer so an actual like pointer sized value to any function that has the signature one u32 in and one u32 out and i can call baz with bar u32 i can also call it with bar i32 in both cases what happens here is that the compiler coerces the function item type into a function pointer type so that this function can be called so inside of here if i now print size of val of f and run this what oh zero that's fine um you'll see that here the the size of the value is eight because it's an actual function pointer and that's necessary because these two functions are distinct from one another right there are two instantiations of bar for different types which means they have different code right they've been optimized separately they're just they are different chunks of code that happen to share sort of a generic name um but we need to pass the the pointer to the first instruction of one instance of that function um when we call bass the first time and a different function pointer the second time around but this doesn't allow us to be generic if you will over which exact function is passed in as long as it has the same signature right and and of course if you passed in something that was the wrong signature like if i did this right then now it wouldn't compile because it'll tell me the it expected a function pointer to a function that had the signature u32 and u32 out what it found was a function item that has a different signature that is it's a void function it takes no arguments and returns no value and crucially here you see it doesn't coerce here because it doesn't match the required type but what we learned from this is that function items and function pointers are different from one another but function items are coercable into a function pointer and part of the reason we need this coercion is because if i do this if i didn't ever call this right then now even though i've named the function bar with t instantiated to i32 the compiler doesn't actually need to generate the code for that function it's no under no obligation to because it's never called however here i'm calling baz right and so and i'm passing in this coerced to enter because it needs to be able to produce a value that's a pointer to that body and therefore it has to generate the function so that it can get generate that function pointer in the first place of course if the whole thing get that code eliminated then that one would happen but but in general this forces the compiler to actually have to generate this function body um in general of course you'll do the same here like you're unlikely to just assign it and then leave it but but regardless all right video choppy this time what is going on um huh it's fine now okay it's fine now i'll keep going if it's fine now if it's not fine now i'll stop what do you think fine not fine it's really weird that this is happening i did upgrade obs so maybe it's obs um that's very very strange fine on twitch okay um so the the takeaway from this right is that a uh function item uniquely identifies a particular instance of a function um whereas a function pointer is a pointer to a function with a given signature and you can turn one into the other but not go the other way around and this of course would would then work again um all right so uh that's function items of function pointers and then we have this thing called closures um so you might have seen uh is isn't quacks is like the next uh metageneric type i think um so this takes an f where f and let's start out with is just fn okay so with bar u32 what does this do so this also works and what we're saying here is that quacks is a function that is generic over sum f where f implements the fn trait and notice that fn with capital f is different from the lowercase fn that we use for a function pointer although the construction is the same so if i wanted to say it takes a u32 and returns to u32 i i do it in the same way but one is a trait bound and the other is a function pointer type so this is not a type this is a trait and here there are three different traits so if we go back and look at the list of operations there's fn fn mute and fn once and the distinction between these is the definition of the trait specifically fn takes a shared reference to self f and mute as its name applies takes an exclusive reference to self and fn once takes an owned reference to self and the implication here as the name implies for fn once you can only call an fn once a single time and at that point you've moved the value of the the function that you wanted to call and you can no longer call it again um f and mute you ha you can only call once at a time um so you need a mutable reference to it in the first place and if you have one you can call it multiple times but you can only call it once at a time and this is important for example if you stick an f and mute in an rc you wouldn't be able to call it similarly if you given a shared reference to something that's f and mute you also cannot call it this is also why in general an f and mute if you passed it to multiple threads like through something like rayon then you couldn't call an fn mute for multiple threads at the same time because it wouldn't be exclusive fn on the other hand is sort of the equivalent of having a shared reference in that you can call it multiple times and you can call it multiple times at the same time or at least through a shared reference and uh the the reason for this will become clear once we look at what closures are and what they do but let's first think specifically about what this means for function items and function pointers so a function pointer uh and in fact a function item has no state right they are standalone just chunks of code and they don't reference anything in any other stack frames right they don't reference any memory that's stored outside of themselves there's no lifetime associated with a function pointer or a function item is the other way to think about this and what that means is that they don't really care about self right like for them all self contains is well nothing really for a function item and for a function pointer self this is the pointer to the function um but there's no state to really mutate there there's nothing to move and therefore function pointers and well function items cores to function pointers and function pointers implement all three of these traits um so if you have a function pointer you can pass it to something that takes an f and once you can pass it to something taken f and mute and you can pass it to something that takes an fn um the one way to think about these fn traits is that they are sort of a um a hierarchy where uh anything you can you can almost think of it as here implement fn once or implement fn for f where f implements fn mute we wouldn't actually ever write this but you you can think about it this way and the reason you can think about it this way and i'll show you in a second uh and take some arguments i am lying it should be this so uh if we go back here to yeah so as you see in the documentation do since both fn and f and mute are sub traits of n1s any instance of fn or fn mute can be used where fn once is expected so in other words fn implements fn once so let's start from there and then we can and the reason for this is if it requires um if if we're given an owned version of self that is so far we've only talked about function pointers but if we're given an old owned reference or an own self rather um then we could trivially produce a reference to self so i can really then do um the sort of fn call of self like i can easily translate one to the other right because if if i have something that implements fn and i'm given a self i can just take a reference to it and pass that to the fn and similarly i can do the same thing for something like f and mute if i add an f and mute i'm given a self right so i'm given ownership of it i can trivially just generate an exclusive reference to it and similarly f and mute i can easily implement for fn right so i'm given a mutable reference to self well i can just turn that into a shared reference and then call the fn version right because these are sort of strictly more powerful um is that does that make sense like why these end up forming a hierarchy so anything that implements fn also implements f and mute and fn ones anything that implements f and mute implements fn once as well but not fn because you can't if you're given a shared reference you cannot produce an exclusive reference and if and anything that implements fn1s only implements fn once all right does that make sense so far yeah i don't know what's going on i think it's my internet that's being weird um so we have this hierarchy and and i think it'll make a little bit more sense why why this hierarchy is is needed once we start looking at closures because again a function pointer implements all of these because it doesn't actually care about the ownership of the function pointer because it's just a pointer to code you can think of it as a function pointer implements fn and therefore it also implements f and mute and fn once but by virtue of the same rules we just talked about and so that's why here uh when we require an f that implements fn we can easily pass in a bar u 32 because it coerces to a function pointer which implements fn and similarly if i made this f and mute this would still work and if i made an f and once this would still work because function pointers implement all of them in terms of actually calling them though you'll see that here let's say that i wanted to actually call the f that i was given if i have an fn once then i need to take it by ownership if i did this for example reference to something that implements f and once then the compiler won't let me call it because fn once requires ownership of the f it requires that i be able to move the f into the call which it's telling me it can't do because it's behind an exclusive reference right and now similarly if i get an fn if i say that it's an fn mute then now all i require is an exclusive reference which i have and therefore i can call it but if i got a shared reference i couldn't call an f and mute because it can't be borrowed as mutable which is required in order to call an fmu but i could if it was fn right and so then we've ended up going full circle here um all right so let's now go back to why are these different like we already mentioned how function pointers implement all of these by virtue of implementing fn so why do we need this distinction the decision comes up once you start talking about closures so fundamentally a closure is written this way um so as you've almost certainly used closures if you haven't this may not be the stream for you um but basically it takes arguments uh which are passed in between these uh and it has some body that returns the contents of those let's say here we can annotate these with it takes an i32 and i32 right so this is a closure this happens to be a non-capturing closure so in general closures are named closures because they close over their environment that is they're able to capture things from their environment and generate a unique function if you will that specifically absorbs or uses or references data from the surrounding environment at the time when they're created in this case it doesn't capture anything from the environment it only references its own arguments and therefore this is a non-capturing closure and what's interesting about non-capture enclosures is that they are coercible to function pointers so i can actually call baz with f if it had the right signature which we can make it do so see here baz takes a function pointer and closures that do not capture over their environment can be coerced to function pointers they can also be they also implement the standard sort of function traits right so they implement fn for example and because they implement fn they also implement fn mute and fn once and all the way up but if i have a closure that does capture from the environment so let's say that i declare a z which is a non-copy value so it's going to be a string new if it's copy i'm going to hand wave a little bit and say that we don't want it to be copy because it makes us reasoning more complicated than it needs to be so here i'm going to declare this closure as just consuming z um and you'll notice that now it it no longer can be passed to bass and if we look at the compiler errors um you see that it says closures can only be coursed to fn types if they do not capture any variables and in this case it does capture a variable right it captures the z variable from from its own stack now let's say that all the closure did was like print line z right so it doesn't actually move z it just takes a reference to z but nonetheless it it captures over the environment and that means that you cannot represent this closure as just a function pointer because in order to like this this closure think of it as the compiler generates a sort of anonymous struct for the closure that contains fields for everything it captures from its environment because when this closure runs it needs access to that information so you can sort of think of this as it generates like a f closure struct that has a z field which is a string and crucially right this is an a or if you will scope which is a reference to the surrounding scope and then you can think of it as it implements fn for f closure and it can do that because if it's given uh a shared reference to self then it can still call that closure in the sense of this implementation is going to be sort of copy paste from uh closure definition right it'll just turn that into self dot z and that works just fine because this is just a shared reference anyway so you can you can't actually write this code but you can think of this as what the compiler generates and therefore you see why it needs access to the z the function pointer which would just be a pointer to like the start of this code block would not be sufficient it needs the additional state of this z does that make sense so far okay so so now let's look at what happens if i make this mutable and i say that this is going to z dot clear so clear is a method on string that takes a an exclusive reference to self and it clears the string um it doesn't deallocate anything but it clears the string so now the f closure that gets generated the sort of state for the closure has an exclusive reference to a string because that's what it needs to capture in order to call clear right it can't capture a shared reference because this requires an exclusive reference and at this point um if if we now again imagine that it sort of copy paste the the body this clear won't work right self.z dot clear and the reason this isn't going to work is because the z here requires an exclusive reference but all we have is a shared reference to self to the the sort of closure state if you will and so we can't actually get an exclusive reference through through that even though we have one here this ends up being shared reference to an exclusive reference to a string which is only usable as a shared reference to a string and so this won't compile or in other words when you have a closure like this it cannot implement fn it can implement fn mute right because here we now an exclusive reference to the closure state which has an exclusive reference to the z and therefore we end up having an exclusive reference to the string and this works just fine and again anything that implements f and mute implements f and once and so therefore this would also implement fn once and so so this closure is an example of something that implements let me comment this out just to avoid the compilers for a sec um so you'll see that this doesn't course to a function pointer right anything that captures cannot be it we we try to call quarks with it but it says the closure is f and mute because it mutates the variable z here right it expected a closure that implements the fn trait but this closure only implements fn mute and that is indeed exactly what's going on as we just discussed it cannot implement fn because it needs to mutably borrow from its environment right if i made this take an f and mute then i would need to take at least this or i could just change this to be owned right then now this and i don't pass in a shared reference to it so now um now f is just fine to pass to quarks because all it requires is a uh an fn mute and this closure is f and mute what then you might ask is okay what what about this fn once well let's imagine that what we wanted to do in here was actually drop z so to drop z we need to have ownership of z which means that we need to move into the closure right so z gets moved into the closure and that means that you just like obviously cannot call this closure more than once because to call the closure you move z but if you tried to call the closure again you would have to move z again but you can't move z z again because it's already been moved or if we expressed it this way like we've been talking about so far this no longer has sort of a lifetime here it it owns the z string right or you can think of it as it still has scope but there's just no use for that scope if we try to implement f and mute and this tried to call drop self it's not possible for us to drop self here because calling drop requires ownership of self but all we have is an exclusive reference therefore this won't work we can however implement effin once because if and once is given ownership of self and therefore we can drop uh self.z sorry this should have been uh self.z all along so here we are allowed to because we get ownership of the closure state and so this implements f and ones but it cannot implement f and mute and it cannot implement fn um and you can see this also by if i comment this out again because you can't actually manually implement the fn traits at the moment you'll see here that it says that we can't this call to quarks is not valid because it expected a closure that implements the f and mutate but this only implements fn once and it implements fn once because it moves the variable z out of its environment here so that's sort of the the path we go around here and you might have seen move closures so you can write the move keyword before the definition definition of a closer to closure and what that means is so closure capturing is actually a little complicated um because here the compiler sort of knows that what the closure needs is an owned version of z it needs to own z and therefore it it determines that it should move z into the closure when we called z dot clear the compiler sort of automagically figures out that all we need is an exclusive reference to z and therefore it only needs to move a an exclusive reference into the closure and not all of z itself and similarly when we had print line of z it realized that all you need here is a shared reference and therefore it only moves a shared reference into the closure rather than all of z and that logic generally does what you want but but it's not perfect and there are some cases where you want to move a value into the closure even though you don't technically need it an example of this could be if the closure you want the closure to be the thing that drops the value that is when the closure exits you want the value to be dropped so currently the way this is this is organized is that z the string will not be dropped until the end of this scope down here right at the end of main because it's not dropped in the closure if i write move here what i'm telling the compiler is move z into the closure and so now z is dropped here we still only needed a shared reference we're telling the compiler i want you to move into it here and now because we're moving z into it we end up in the same state of we actually need to own the z and so we can only implement f and once we cannot implement f and mute or fn that make sense actually i don't know if why does this compile oh maybe i lied to you i think i lied to you i think the move it'll still apply this heuristic uh yeah you're right the the reasoning for this is actually slightly different which is well that it is true that you can make it drop in here but but it's also because if i do this the lifetime of the closure is tied to the uh to the stack here um so so maybe if i if i give a slightly different example um so make ifn is going to return an impul uh fn once sure why not and it does z equals string new and then it's going to return a closure that is going to print line z right so here is an example of i want to return a function right i'm not going to tell the caller whether it's a closure or not but i'm going to return something that's callable and it's only callable once and in fact let's say fn i want to return something that's callable and i want it to be callable multiple times which this one is right it's just printing z over and over the problem is that right now this borrows z right and so the closure that's returned sort of has a lifetime that's associated with the the sort of self of this function right this you can sort of read as z is like a reference to z that's moved into here and this reference has some lifetime right which means that this closure type really has that same lifetime but if you don't specify a lifetime it assumes that it's static and so here we're promising to give back something static but the closure that we return isn't static because it references this z and so there you can apply the move to say move z into the closure and now this closure is fn because every call to the closure does just references the string that's stored in the closure self contents and that's fine right you can have multiple calls that all get a shared reference to the closure state which gives a shared reference to the string for printing if it would not work if this tried to say drop z because it wouldn't implement fn right because once you've called it once you've now moved out of self and so you cannot call it again and so this is where move is is useful now one downside of move is that move means move everything there's no like move like let's say we had um x as well this will move both x and z into the closure and that that is oftentimes what you want right in here we wanted to return something that was static but that's not always what you want um but the way that you work around this is you either don't use move right if if you can get away with it or you use move and if you if there's something you specifically want only to be borrowed you do like um let x2 equals reference x and use x2 in here now this this won't work in this case because this makes it no longer static but that's the way that you can move some things by reference and some things by ownership uh and there are many ways to express this like sometimes use shadowing if you don't care about the old value you could introduce a new scope so that you don't have to deal with this later regardless of how you end up doing it this is the way that you would move some things but borrow other things into the closure does that code create a new string in static every time make fn is called yes so the way we had this if i go back here and remove z and make this pub i guess this every time you call make fn it's going to allocate a new string and then return a closure that owns that string that was allocated you can't choose per variable which to move or not move move is all or nothing so that's why you end up with a pattern like this for example to specifically say the thing that i want to move in here the x is actually a reference to the real x so i want to move the reference and of course if you do move something into a um into a closure then now the closure as as we saw up here right the closures collectively or the closure owns the string and so it's only when then any call to that closure uses that same string and it's when that closure is eventually dropped like the actual variable that holds the closure or the allocation that holds the closure when that gets dropped the state structure for for that closure gets dropped and therefore the string also gets dropped um all right uh so we've now actually talked most of what's important to grasp about these function types but there are a couple of more things i want to touch on the first of these is around din fn uh so tin fn like so uh so just like with other traits uh you can also use the fn traits through din to get dynamic dispatch um and you specify the full function signature and you just put in in front of it and this works just fine um in fact here i can just call f and similarly if this was a din fn mute i can call it as long as i take this as mute right so that works fine and if it's an f and once i can also still call it so so far so good this was not always the case interestingly it used to be that there was this special fn box trait and the reason for this was that this is a little bit of an anecdote but but it is an interesting one i think it used to be that box didn't fn once uh and in fact box didn't fn anything did not implement fn anything so basically it used to be that box din fn did not implement fn and similarly for f and mute and fn once and the reason for this actually is kind of interesting so let's imagine that we were the people trying to implement this because i do think this gives a a sort of interesting insight let's imagine that we are the standard library and we want to implement uh let's say fn for a box din fn so we have to implement call we take no arguments we return nothing and now the question is what do we put in here right so self is a box but we have this challenge here of like let's say i do self self.0 let's say that's how i get at what's inside the box and i want to do call with no arguments right so it seems like this is all i should really need to write sort of de-reference the box and then call the thing that's inside the the problem is what is the type of self.0 here if we were to write this out as a variable right and let's let's take fn once just because it's more clear what's going on so i move out of self.0 to get at the thing that's inside the box uh and then i do x dot call what's the type of x the type of x here is din fn once but this type is not sized so how much space does x take up on the stack here remember din in general is unsized right it's it's not sized um and that's why in general for box you always need either a reference or an exclusive reference or something like a box around it you can't have a free standing din because it's not size so the compiler wouldn't know how to range the stack frame for this method call and so it's kind of interesting if you look back sort of historically and this is going to be bright for a second i'm sorry about that um so here in the release announcements for rust 135 you see it was like a big thing that now these traits are implemented for the appropriate box types um and previously this is evan box that got to do special compiler magic and as you see here this sentence this was ultimately due to a limitation on the compiler's ability to reason about such implementations which has since been fixed with the introduction of unsized locals and so someone said why couldn't you take a reference to the box contents well this is why i chose fn once because here to call it you need an owned self so you can't take a reference to this because if you had a this you couldn't call it because fn once requires ownership of self and so slightly similar issues arise with uh fnf and mute um and so there is actually an rfc specifically for unsized r values which is required for this implementation to exist and this rfc the rfc has landed but there's lots of implementation questions and basically the compiler gets to take advantage of this particular feature but it's unstable so like you can opt into it on nightly but you can't generally use this on your own code um on stable but it is required for this particular thing to work which i thought is like an interesting tidbit um and there's all sorts of cool implications of what we could do if we got unsized r values but but that's uh neither here nor there um so when you have boxton and then some fn trait that just works now you don't really have to treat it specially like in general din just works but you do have to keep in mind that if you get something like a um uh how am i going to demonstrate this best um so this implements fn so let's say i here gave a din f uh then equals so this works just fine right i turn f into a dynamically dispatched fn this is sort of like a function pointer except it's allowed to state right so it has a it has a self um basically it constructs a cell for this closure and then uses that as the data pointer of the v of the dynamic dispatch and the v table just has the call method so that's so fine as far as fine the challenge here right is let's say that i try to make this fn mute so now i have a a din f and mute but all i have is a shared reference to it and therefore i can't actually call it because this doesn't implement f and mute this only implements fn because all you have is a shared reference so when you use um dynamically dispatched function traits you need to make sure that the sort of the wrapper the indirection type the the wide pointer type that you use allows the kind of access that you actually need in order to call that function right so in order to call an fn mute you need to stick it behind a shared reference in order to call an fn all you need is a shared reference in order to call an fn once you actually need a wide pointer type that allows you to take ownership right so i mean i would have to box new and of course again the same hierarchy applies so if you have a wide pointer type that allows you to take ownership then it will also work with any of the sort of weaker function or um less restrictive function uh traits so i can have a boxed in f and mute because if i can take ownership then i can certainly get an exclusive reference if i can take ownership then i can definitely get a shared reference so these are both fine and we'll implement the appropriate trait outside of the white pointer and if you think of something like arc right so uh standard sync arc standard sync arc will allow you to um to stick uh unsized things into it but an arc but by necessity will only give you shared access to the thing that's inside and therefore arc din of fn implements fn but arc din fn mute still only implements fn or in fact then can't implement the trait rather so here if i say this takes fn uh maybe they haven't implemented this for arc maybe they've only implemented for box interesting so i'm guessing that this implementation doesn't actually exist for um for arc yet um if we go back to look at um [Music] fn for example you see there's an implementation of fn for box of f where f f implements fn but there is no implementation of arc i wonder why that is that implementation should exist interesting right and we can use the intuition we've built up so far for why that should be the case an arc supports unsized values right basically r can support being a wide pointer and therefore it can hold the din fn and if it can hold the din fn and it's able to give you a shared reference to the closure state then it should be implemented to implement fn because all that requires is being able to get a shared reference to the closure state um so this suggests that there's sort of an implementation missing here and it might be because um because of this issue we were looking at about unsized r values maybe it's like specialized to box somehow but that would be an interesting implementation to add um that's pretty good okay so about an hour i think i guessed about right the the last thing i wanted to touch on was um const offense so what's a good example of this let's try to erase some of this stuff okay so let's say i define a closure that takes uh that just returns zero right so this closure is a constant closure you could evaluate this closure at compile time right this is sort of equivalent to cons defend like make zero that returns a u size or i guess i 32 is the default right these are both the same in the sense that they're both callable at compile time they're basically both const but the question becomes let's say that i want a const i fn know foo and i wanted to take an f that's let's say fn once and i want my const of n to be able to call f well that's not currently okay because the compiler doesn't know that f is callable as a const right because we've just said this is an any any type that implements f and ones and we don't know that the implementation of fn once is actually const so there's nothing unstable that you can do about this today but there's a sort of interesting pre-rfc discussion that's going on and has been going on for a while about is there a way to say i want to take i want to be generic over any type that implements this trait but only using things that are const evaluatable and the syntax i've come up with is this and let's see if i can opt into this easily i forget what the actual thing is called um it might not be named anywhere that i can easily get at what happens if i run cargo r let me just pull this up real quick to see what the tu tu tu what is the name ah future to concentrate impul so we're going to add this here and then we're going to rust up override set nightly and we're going to cargo r and see what happens um oh yeah there's here there's the second one you need to add which is this one so again as you can see this is very experimental okay so um so what this signifier means this this cons this this tilde cons is that it doesn't actually mean this f and once or this f must be const it doesn't mean that i will only take types that that have a constant implementation of f and once that's what this would mean right this would mean f must have a constant implementation alpha and once it's also not question mark like you might think of like question mark sized it's not really question mark cons because it's not saying may or may not be const which is what question mark sized means rather what the tilde here signifies is foo will be const if f is const if f is not const then fn is not const and we haven't talked too much about constant stream but the basic premise here is if you have a const fn it's callable at compile time or at runtime like you can call it either of them which sort of means that it's a const of n is also an fn and what this is saying here is that this const fn is only const so it's only callable to compile time if its generic parameter is also const in its implementation of fn once and so that's why this is like new sigil here because it doesn't really mean the same as the other sigils that we have um so that that's that's why i mean this this is not a full rfc yet it hasn't been merged so this is very very experimental as you can see with like the two features we need to opt into um but but it is an interesting thing to keep in mind and something you will see going forward as we start seeing more and more classification of rust code um uh foo calling x oh sorry yeah so if who calls x here um what am i missing uh expected zero arguments yeah i think this is just rust analyzer being confused because it doesn't know about these features right so this runs just fine um ooh why oh because string new is const if i said string from foo this is not const oh a string from const ii uh what is not const veck one two three what is not const oh sorry it's fine okay so right so this is what i was yeah this is what i was getting at um let's let's uh do con cons of n test foo that's what i need to do um the the problem here right or the reason why i have to do this is because main isn't const so there's no requirement that foo is const so i i think this can go back to a string from foo now a string u is const so in maine i'm allowed to call foo because i don't require food to be const i can call it even if it's not const and the closure here so the implementation of fn1s is not const and therefore foo sort of genericized over this closure is not const but that's fine for main so if i comment out this for a second you'll see that this runs just fine but in a const fn in a const of n you can only call things that are themselves const so here if i try to run it you'll see that it complains expected in fn1's closure found this closure and here you'll see the errors aren't very good and that's because it doesn't the error reporting doesn't know about this const flag yet but the actual complaint here is that cons defends require that everything they call is also const so this requires that foo when generic over this closure is const but this closure's implementation of fn once is not const therefore foo is not const and therefore this call is illegal if on the other hand i use string nu here which is const ah is maybe not const ooh interesting that's interesting does it work if i do this oh weird yeah i think this is a evidence of this being a very experimental feature but the intention is that if the closure is itself constant or constant evaluatable um which string new is because i believe string u yeah string u is a const fn um then foo should also be const fn uh and therefore should be callable from cons context constant const events are stabilized but this kind of bound is not stabilized and so error reporting doesn't know about this kind of bound and so it doesn't really talk about it in the error message that you get it doesn't know about constant more generally all right um i think that's all i wanted to cover about functions and closures and the the traits and types that are involved are there questions about any of the stuff that we've talked about so far um like any of the any of the bits that you want me to go over again and i'm sad about the audio issues but hopefully at least in the video on demand i'll chop it up a little no people seem to be happy you've generally followed any questions about anything else now that we we ended up with sort of a relatively short stream which was kind of as expected um anything that i can answer outside of this particular topic um sometimes you end up with complicated lifetime bounds with 4 r um so let's get rid of the const of n stuff for a second um okay so an example of this is uh if i write a f and it takes an f uh and i want to say that f is a function that let's imagine that it's a map function so it's going to be given a sort of uh i don't know it's going to be a given a reference to a stir and it has to return a stir so let's imagine that i i want to write a function like this which maps a string and i want to try to call quarks with a closure that takes x and returns x uh so far so good this is not a problem this this works just as expected um but there are cases where this gets a little bit more complicated in particular like remember in bounds you usually although not always but you usually have to specify lifetimes what are the lifetimes here right whenever something returns like you can often omit the lifetimes but if we were to try to specify what the lifetimes are here what are they because there's no lifetime here if we tried to fill in the lifetime for this for this trait bound what actually is it there's no tick a because we can't assign a tick a here what we're really saying is we want f to sort of reuse the lifetime that it gets in in its output we want to say that it's allowed to reference the same thing that was referenced in its input and this is where you get this special for syntax and what this syntax means this is the actual de-sugaring of that syntax um this you can read as for any lifetime tick a f is an implementation of a function from a stir with a stir reference with a lifetime of a to another stir reference with the same lifetime of a so it's not actually that's comp that complicated it's a way to say that this this needs to hold for any lifetime this is what the bound should be it's very very rare that you actually need to give a forebound like this it does sometimes happen if you if you have trait bounds that have lifetimes but are not offense the compiler is pretty good about inferring it for anything that is um of fn type or f and mutant effort once it can usually figure this out but once you start having other traits that aren't fn in here you sometimes need to reach for four but but it should be very very rare um will you be doing another q a session sometime i will but i don't quite know yet i really want to get back to the hazard pointers library um so i'm trying to find time for that too uh how much will i enjoy rust for rust stations uh over 9000 uh how do you broadcast both video and your screen at the same time what software using i'm using obs very happy with obs if i want to pass a closure to an asic function the closure needs to be static right how does this kind of closure capture its environment um if you want to pass a closure to an async fn you can just do so there's nothing preventing you from taking any fn it doesn't need to be static it's more that usually with usually with futures especially if you want to do something like tokyo spawn of the future you get back then tokyo spawn just like thread spawn requires that the argument is like static and if f here is not static then the return future will also not be static right if we sort of think of the de-sugaring of this right it's fn this to impul future that's really the de-sugaring of um of this and impul trait just like async fn automatically captures the lifetime of its inputs so if this input is tied to some lifetime then the output type will also be tied to that same lifetime which means it will not be static unless the input is static and so that's why you often end up having to add static to generic types that you pass into async functions it's not because they're required like if i um if i just directly await it here then there's no need for the function to be static the future that's returned to be static it only comes up if you try to do something like spawning where the future needs to live longer than the current stack frame um you often need to pin it i mean you should very rarely need to pin things manually in general a weight syntax should take care of you um all right in that case i think we're gonna end things there uh thanks everyone for watching i hope you learned something uh go teach someone else what you learned and i will see you all in a few weeks i really want to do more um hazard pointers but i just need to actually find the time to do six hours of coding all right bye everyone hope you enjoyed it
Info
Channel: Jon Gjengset
Views: 13,408
Rating: 4.9585061 out of 5
Keywords: rust, live-coding, closures, functions, function pointers, function items
Id: dHkzSZnYXmk
Channel Id: undefined
Length: 66min 40sec (4000 seconds)
Published: Fri Oct 08 2021
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.