thats the thing i dont like about rust, i mean after years of writing typescript i dont think i would ever find a piece of code in some lib i couldnt just look at and understand, but in rust i feel like i m absolutely not gonna get some of these never ever xD but i can write my snake app in rust and call it an achievement and that gives it a point back :D
imagine me, a fresh newbie that just started learning Rust jumping into this video. I'm half devastated of how complex this is but excited at the same time, it's gonna be actually fun to learn this well in some future.
@@cosmiclattemusic pretty sure even if u gonna work with them and use well u still would return back to lifetime docs & explanation videos like i do sometimes)) so don't worry
@@everythingbuttherain6323 also you don’t need to learn this at the beginning. Just start with the things that apply to every language, understanding the standard library, then go on to more complex traits and lifetimes topics. And finally learn every low level detail, like this or atomics, where I find reading the source of hugely popular crates together with the std doc and the book side by side is the best way.
**Summary** - Rust drops in revers order - The first implementation, the formatter function's lifetime annotation is telling the compiler that any reference passed into this should live as long as the formatter method live - Due to the reverse order drops, the str variable dropped before the formatter - Redefined the lifetime annotation by using for
Thanks to you, i learned memory management, ownership, and borrowing, passing a large portion of the rust learning curve and getting a deeper understanding of low level programming 😀
Amazingly well done! I found the first half to be a great pace, the second half was a little fast - need to take a breath when ending a complex statement - and a second or 3 more for the code to be on screen. Very advanced topic which is very helpful to have straight forward examples of :)
Now that you are on a roll about generics and traits, it would be awesome to have a video on "magic handler functions", where a handler trait is implemented for closures based on the arguments that conform to a common trait. This then allows the user to define a function whose arguments uses types to "extract" data from a context. Notable examples of this pattern is seen in Axum's extractors and Bevy query and systems. Albeit there are some downsides to the pattern, it can form a nice API without having to do all the plumbing yourself.
For anyone looking for a decent explanation, there's a mini book called "Dependency Infection Like Bevy From Scratch" which covers the basics of the pattern
Thank you so much for making a video about this! It's hard to get into and understand high-level concepts like this without a great introduction as you presented it. I have to admit, I'll still need time to wrap my head around all of the implications, but here's a tip for other's who are also struggling. Remember when you have a trait with some methods, you can attach the generic to the trait/trait implementation itself `trait MyTrait { ... }` or you can attach the generic to only the method of the trait `trait MyTrait {pub fn method() { ... } }`. So the first example, where the generic (and its trait bounds) are defined on the struct/enum, means that the generic is unchanging for a particular instance of that struct or enum. In the second example, the generic and trait bounds are unchanging/static for a particular call to a method; this includes the values passed to the method and returned from it. I think higher-ranked trait bounds are the next "russian doll" in this example. So higher ranked traits do not restrict the inputs and output of a method, but instead restricts aspects of the output with respect to itself. An example would be a closure returned from a method, where the closure's arguments are constrained somehow, but not constrained at the time when the method call was made. I write this without a full understanding, so I think the only real takeaway is the russian-doll analogue, if that even helps any, lol.
This is the video I was waiting for 🔥 I always search for explanations on the more complicated stuff in rust but a.) there are not many and b.) most of them doesn't explain it very well or deep enough. I'm also looking for videos who explain functional programming approaches in rust more deeply but there is also not much out there in a digestable format. Anyway, thank you for this video, I'm very thankful you put so much effort into explaining complex stuff!
Oh i think i got it earlyer. The thing is that a generic type can be restricted by a trait like: fn add(left: T, right: T) -> T { a+b } I didn't test if this compiles. But than i realized that an output type can be different.
Thats very cool thankyou :) Whenever I am writing rust i have this feeling that i should be able to convice the borrow checker of a thing, but I only succeeed about half the times that I should. Hopefully this helps next time I get stuck :)
5:10 the lifetime is *not* linked to that of the `impl Fn()` (that would be `impl Fn() + 'a`) - instead, it is inferred at its first call site to be 'static, which is longer than the second reference is valid for
one of my grievances with rust is the multiple ways you can do things, the example with trait bounds sums it up, you have this relatively complex thing for beginners, and you have 3 different ways of doing it, in the template, with where or with impl it adds much more overhead to reading and understanding code for beginners, one way should be decided on and move on
I think I used this feature once. And before I used it, I wanted to use it, because I thought I had seen it and hoped it would be helpful, but then it seemed like it didn't exist and I thought I just misremembered.
The function contract says the returned reference must live at least as long as the input reference; hence the returned reference may only be used while the input reference is valid
2:20 I prefer to think of it as fn first_word(s: &'a str) -> &'b str where 'a: 'b {...} We are specifically _not_ requiring that the input variable and the output have the _same_ lifetime. So clarifying that they are different is much clearer and more transparent than writing it as though they are the same and letting the compiler silently and invisibly subtype it properly for us.
Lifetimes are covariant, which means the two are equivalent. IIRC the reference explicitly states that fn(&T) -> &U is equivalent to fn(&'a T) -> &'a U
@@Starwort I'm aware. I just think lifetimes are difficult enough as they are and we don't have to throw in covariance into the mix at the same time, especially when teaching it to newbies.
@@kadercarter borrow checker is simple really. It requires some mental shift when you come from another language, but once you get it, it will be really easy. But macros... oh boy
Macros seem the most complicated for me. Mostly because there's no real good tutorial that goes step by step as to what each part of the macro does and then showing what Rust code it expands to and why.
The association of the lifetime of the reference argument to the lifetime of the closure *seemed like* an unfortunate, unnecessary, and counter-intuitive language constraint. However, in the case where the closure collects the provided input such as in an internal stack (aka, FnMut) then such constraints would obviously make sense. With that insight, I suppose the possibility of dynamic mutability within an Fn or FnOnce closure's would require persistence of the provided reference. Nice. Thanks.
Except it isn't true. The example at 5:10 is actually just the result of inference. A FnMut with an internal stack would be `impl FnMut(&'a T) + 'a`, tying the function's lifespan to its inputs' lifespans
Your paid portal isn't sending me a reset password link. My original password, which I never changed, doesn't work anymore. Can you please advise or assist me with my issue?
It's because Rust actually cares about safety and correctness. In most languages lifetime annotations only exist in comments: "This function must not be called after close() has been called for the handle" etc. In Rust, this is encoded in the function signature. Lifetimes are necessarily part of a function's public API: they set boundaries on what callers are able to do with the arguments and the returned value - and what the function itself is able to do internally without breaking the calling code.
@@LemurFromTheId sometimes the compiler message makes me a little less confident. but it makes me improve myself when writing programs other than rust. shadowing, clone, and error handling make me a little less confident when writing code other than rust language. because my code looks very bad and not confident. i don't know if it really needs to be taken care of, because most drivers are made using c language and they are fine
@@affanyunas7693 Less than 2 weeks ago, a memory safety bug in driver code brought down millions of computers around the world. I'd rather we switched to using languages that had better memory safety guarantees than C if we're going to execute them in kernel space. Systems programming with lifetimes is annoying. Systems programming without lifetimes is dangerous.
Get your *FREE 4-Day Rust training* :
letsgetrusty.com/bootcamp
I like how even after learning rust for over a year lifetimes still scare me deep down, because of stuff like this.
thats the thing i dont like about rust, i mean after years of writing typescript i dont think i would ever find a piece of code in some lib i couldnt just look at and understand, but in rust i feel like i m absolutely not gonna get some of these never ever xD but i can write my snake app in rust and call it an achievement and that gives it a point back :D
Lifetimes? Whats that, is that what happens when you call .clone()?
@@11WicToR11yeah typescript doesn’t require a lot of understanding of anything
@@lobotomy-victim thats just a perspective statement, good for you that you left us in the dust
Nothing to be scared of! Most of the time you don't even have to worry about lifetimes :)
2:06 holy cow this graph is so descriptive and the relation between them at the same time is godlevel pedagogy. tysm
I got you :)
too complicated for me but thanks
edit: i think i get it now 🔥
Awesome to hear :)
imagine me, a fresh newbie that just started learning Rust jumping into this video. I'm half devastated of how complex this is but excited at the same time, it's gonna be actually fun to learn this well in some future.
@@cosmiclattemusic pretty sure even if u gonna work with them and use well u still would return back to lifetime docs & explanation videos like i do sometimes)) so don't worry
@@everythingbuttherain6323 thanks for the really accurate advice ✨
@@everythingbuttherain6323 also you don’t need to learn this at the beginning. Just start with the things that apply to every language, understanding the standard library, then go on to more complex traits and lifetimes topics. And finally learn every low level detail, like this or atomics, where I find reading the source of hugely popular crates together with the std doc and the book side by side is the best way.
**Summary**
- Rust drops in revers order
- The first implementation, the formatter function's lifetime annotation is telling the compiler that any reference passed into this should live as long as the formatter method live
- Due to the reverse order drops, the str variable dropped before the formatter
- Redefined the lifetime annotation by using for
Thanks to you, i learned memory management, ownership, and borrowing, passing a large portion of the rust learning curve and getting a deeper understanding of low level programming 😀
Glad to help! Comments like this keep me going :)
I've never really grasped HRTBs despise reading several explanations, but this video finally made it click. Great work!
Amazingly well done! I found the first half to be a great pace, the second half was a little fast - need to take a breath when ending a complex statement - and a second or 3 more for the code to be on screen. Very advanced topic which is very helpful to have straight forward examples of :)
I FRICKING LOVE TYPE LEVEL PROGRAMMING I CAN’T GIVE ENOUGH TYPE LEVEL PROGRAMMING
You would love Idris. Its types are so powerful that one can prove math theorems at the type level.
@@hermestrismegistus9142 wait you guys are owning your data?
caught another typemasturbator here)
Try C, having to memorize all the type shenanigans doubles the fun :)
Now that you are on a roll about generics and traits, it would be awesome to have a video on "magic handler functions", where a handler trait is implemented for closures based on the arguments that conform to a common trait. This then allows the user to define a function whose arguments uses types to "extract" data from a context.
Notable examples of this pattern is seen in Axum's extractors and Bevy query and systems. Albeit there are some downsides to the pattern, it can form a nice API without having to do all the plumbing yourself.
Yes please I must understand this wizardry
AFAIR, Jon explained this in decrusting Axum: th-cam.com/video/Wnb_n5YktO8/w-d-xo.html
For anyone looking for a decent explanation, there's a mini book called "Dependency Infection Like Bevy From Scratch" which covers the basics of the pattern
I never quite wrapped my head around this until now, thanks very much!!
Thank you so much for making a video about this! It's hard to get into and understand high-level concepts like this without a great introduction as you presented it.
I have to admit, I'll still need time to wrap my head around all of the implications, but here's a tip for other's who are also struggling. Remember when you have a trait with some methods, you can attach the generic to the trait/trait implementation itself `trait MyTrait { ... }` or you can attach the generic to only the method of the trait `trait MyTrait {pub fn method() { ... } }`. So the first example, where the generic (and its trait bounds) are defined on the struct/enum, means that the generic is unchanging for a particular instance of that struct or enum. In the second example, the generic and trait bounds are unchanging/static for a particular call to a method; this includes the values passed to the method and returned from it.
I think higher-ranked trait bounds are the next "russian doll" in this example. So higher ranked traits do not restrict the inputs and output of a method, but instead restricts aspects of the output with respect to itself. An example would be a closure returned from a method, where the closure's arguments are constrained somehow, but not constrained at the time when the method call was made. I write this without a full understanding, so I think the only real takeaway is the russian-doll analogue, if that even helps any, lol.
So, tl;dr. In this case, the "for" does exactly the same as
Ok, this was 10/10! Excellent complexity/time/depth ratio!
Thank you! :)
This is the video I was waiting for 🔥 I always search for explanations on the more complicated stuff in rust but a.) there are not many and b.) most of them doesn't explain it very well or deep enough. I'm also looking for videos who explain functional programming approaches in rust more deeply but there is also not much out there in a digestable format. Anyway, thank you for this video, I'm very thankful you put so much effort into explaining complex stuff!
DUDE ! Thank you ! I was reading some source some time ago and just couldn't find out wtf that was !
would be really cool if you would dive into those codebases like serde and break down how those types actually work lol
This channel is great! Thank you for bringing more of the advanced side of Rust.
I have been programming in Rust for years but have never used this but this is actually really nice and you explained it well!
Thank you so much! I am planning to start implement complicated closures in my crate. This video 100% saved me time in the future
So basically avoid creating code that makes you use this feature, got it.
thanks it starts to make more sense to me now. glad to see more complex rust as kinda stop learning anything new keeping to intro topics
I was definitely missing that part, really appreciate that video! :D
Oh i think i got it earlyer. The thing is that a generic type can be restricted by a trait like:
fn add(left: T, right: T) -> T {
a+b
}
I didn't test if this compiles. But than i realized that an output type can be different.
Thats very cool thankyou :) Whenever I am writing rust i have this feeling that i should be able to convice the borrow checker of a thing, but I only succeeed about half the times that I should. Hopefully this helps next time I get stuck :)
Wow, it's so beautiful and clear code examples 🎉 Good job, Bogdan, thank you 🙏
I've always wanted to understand that better; thanks! BTW, what tool did you use to create this video? Manim? MotionCanvas?
5:10 the lifetime is *not* linked to that of the `impl Fn()` (that would be `impl Fn() + 'a`) - instead, it is inferred at its first call site to be 'static, which is longer than the second reference is valid for
Why is it inferred to be 'static?
@@algorythmis4805 that's how it's first used. Arguably it's an inference bug, but honestly it's a contrived example
one of my grievances with rust is the multiple ways you can do things, the example with trait bounds sums it up, you have this relatively complex thing for beginners, and you have 3 different ways of doing it, in the template, with where or with impl
it adds much more overhead to reading and understanding code for beginners, one way should be decided on and move on
That's pure insanity... I love it. I should really get started with rust.
I had one too many whiskeys for this video. I try again tomorrow
Thank you for releasing a video on this topic!
My pleasure :)
It will be nice if compiler generates error explanation video like yours
Gotta see this video again...
I think I used this feature once. And before I used it, I wanted to use it, because I thought I had seen it and hoped it would be helpful, but then it seemed like it didn't exist and I thought I just misremembered.
2:27 I think what you meant to say was the other way round: The input reference must be valid at least as long as the returned reference?
The function contract says the returned reference must live at least as long as the input reference; hence the returned reference may only be used while the input reference is valid
2:20 I prefer to think of it as
fn first_word(s: &'a str) -> &'b str where 'a: 'b {...}
We are specifically _not_ requiring that the input variable and the output have the _same_ lifetime. So clarifying that they are different is much clearer and more transparent than writing it as though they are the same and letting the compiler silently and invisibly subtype it properly for us.
Lifetimes are covariant, which means the two are equivalent. IIRC the reference explicitly states that fn(&T) -> &U is equivalent to fn(&'a T) -> &'a U
(yep - see Lifetime Elision in either the Book or the Nomicon)
@@Starwort I'm aware. I just think lifetimes are difficult enough as they are and we don't have to throw in covariance into the mix at the same time, especially when teaching it to newbies.
Very good explanation, thanks!
What is the most complicated feature then ?
My best guess is proc macros
I'd assume the borrow checker
@@kadercarter No way borrow checker is this much complicated
@@kadercarter borrow checker is simple really. It requires some mental shift when you come from another language, but once you get it, it will be really easy.
But macros... oh boy
Macros seem the most complicated for me. Mostly because there's no real good tutorial that goes step by step as to what each part of the macro does and then showing what Rust code it expands to and why.
great video, simple and to the point
Idk, I feel like for something to qualify as "second hardest" it would need to be some kind of async ffi proc macro magic, or something
Rust is the hardest programming language for me because I can't get used to this syntax. I'll stay with Go for now. Maybe I'll learn Rust in future
I didn't know this feature existed, until I found it in Rust Nomicon after watching the video. What's the most complicated feature?
Thank you 🌷
I'll remember this video for when i need it later haha
Дякую, Богдане!
The association of the lifetime of the reference argument to the lifetime of the closure *seemed like* an unfortunate, unnecessary, and counter-intuitive language constraint. However, in the case where the closure collects the provided input such as in an internal stack (aka, FnMut) then such constraints would obviously make sense. With that insight, I suppose the possibility of dynamic mutability within an Fn or FnOnce closure's would require persistence of the provided reference. Nice. Thanks.
Except it isn't true. The example at 5:10 is actually just the result of inference.
A FnMut with an internal stack would be `impl FnMut(&'a T) + 'a`, tying the function's lifespan to its inputs' lifespans
Wait what if we actually have like `for T: Read`
There are a lot of complicated features in Rust. I wouldn't say this is close to being the second one. Many things are more complicated than this.
Good content 👍
See new "Let's Get Rusty" video, click new "Let's Get Rusty" video.
I still use Vim/Nvim as my Rust IDE.
Don't flex so hard on us mere mortals
What's the most complicated feature?
That's the joke, to make you ask this question out of curiosity
Gottem!
that's why I prefer C
@@RustIsWinning understandable
have a nice day
Now, do subtyping and variance.
Someone found the hardest feature ;)
so it's like "requires" in c++
Ничего не понятно но очень интересно
Your paid portal isn't sending me a reset password link. My original password, which I never changed, doesn't work anymore. Can you please advise or assist me with my issue?
Let me say, "...which I don't recall changing..."
Hey! Which email did you use to purchase to bootcamp!
@@letsgetrusty I am good Bogdan. You came through in the clutch and I am commenting here to let everyone know that you a good dude!
I still don't understand why I have to use lifetime. Everything was fine before I learned about lifetime.
It's because Rust actually cares about safety and correctness. In most languages lifetime annotations only exist in comments: "This function must not be called after close() has been called for the handle" etc. In Rust, this is encoded in the function signature. Lifetimes are necessarily part of a function's public API: they set boundaries on what callers are able to do with the arguments and the returned value - and what the function itself is able to do internally without breaking the calling code.
@@LemurFromTheId sometimes the compiler message makes me a little less confident. but it makes me improve myself when writing programs other than rust. shadowing, clone, and error handling make me a little less confident when writing code other than rust language. because my code looks very bad and not confident. i don't know if it really needs to be taken care of, because most drivers are made using c language and they are fine
@@affanyunas7693 Less than 2 weeks ago, a memory safety bug in driver code brought down millions of computers around the world. I'd rather we switched to using languages that had better memory safety guarantees than C if we're going to execute them in kernel space.
Systems programming with lifetimes is annoying. Systems programming without lifetimes is dangerous.
is the voice for this video ai generated? or at least ai noise removal?
Then which is the first most complicated feature? Unsafe Rust?
I'd hate to thiink.... this was already harder than anything I've encountered in C++ in 30 years.
can they focus on making the language readable instead
Higher-ranked trait bounds are too high for my high ass
I still don't understand traits. Sorry. Rust is just way too complex. WTF
eh. this problem doesn't exist in c++ which i am thankful for
crowdstrike
What are shtrings and conshtraints? 😂
sometimes it seems to me that rust exists only to demonstrate your intellectual superiority over normies
And y’all still think cpp is harder😂😂…. Saw the func at 0:08 and my brain crashed….well time to go learn zig or stick to just plain old C
Rule number one of Code Club: Never talk about K.I.S.S. -- At least that's what it seems nowadays.
unfortunately, in practice, lifetimes and trait bounds make rust code write-only. It's very bad language design here
Programming in Rust is a huge brain drain - even simple tasks can require serious mental gymnastics.
What a waste of developer time
Is voice in this video AI generated?
no