The magic of Rust's type system
ฝัง
- เผยแพร่เมื่อ 20 มี.ค. 2024
- For today's video, we dissect a tangled mess and uncover its pitfalls. Let's see how Rust's type system empowers developers to enforce crucial invariants, paving the way for cleaner, safer and more robust software.
Free Rust cheat sheet: letsgetrusty.com/cheatsheet - วิทยาศาสตร์และเทคโนโลยี
📝Get your *FREE Rust cheat sheet* :
letsgetrusty.com/cheatsheet
I haven't heard of "parse, don't validate" before, but I love it
Isn't that the zod motto
It has nothing to do with rust, it's haskell thing
@@RandychI mean it's born from Haskell ecosystem's contributor - Lexi Lambda, but tbh applies to any kind of language with a decent type system. I love popularization of this short quote
@@sealoftime yes that's what I actually wanted to say
this aligns perfectly with one of most important software design principles: Make INVALID state UN-REPRESENTABLE.
Do you mean unrepresentable?
@@svenyboyyt2304 Oh, sorry for that. edited.. thnx.
Noboilerplate made a video about this
@@LageAfonso yah, but you can try to apply this using any programming language, some languages help you a lot doing this like rust,C#,typescript.. unlike Go where you should do hacky things to be even close to achieve that.
this may help someone: one of my favorite resources in this topic for typescript is khalilstemmler article : Make Illegal States Unrepresentable! - Domain-Driven Design TypeScript.
This is the greatest start of a video on any language I have seen. Also...I use Arch, btw. Sic Semper Tyranus!
Not the brag, but I'm proficient in Scratch btw
6:32 prase -> parse
"parse, don't prase"
Good catch
This jumped on the new level in terms of the video effects and presentation. Wonderful!
Excellent. I was just settling down to find out how this Rust "type state" idea I had heard of works and here you are explaining it so clearly, right on cue. Thank you.
You should include how your request handler can perform the deserialization for you with serde. With that, you can be certain that the handler itself never even accepts an invalid request. Function signatures are one of the strongest forms of documentation
Well done 💯 The production value and editing on your videos continues to get better and better. This was a good one. It is appreciated.
Great video! 🙌 Would love more like this where you go through best practices with examples
I use arch btw
I use nix btw
do you even vim?
I use Windows 11 + WSL Ubuntu + iOS. 🌞💛
You should rewrite Arch in Rust...
@@maximus1172 I use nixos with my own hydra server btw
(Actually, I don't, I have a pretty janky all in one file nixos config and I don't run my own hydra)
I love these kinds of videos, please make more videos with logical errors and it's fix with code examples
A great example is [u8] and Vec vs str and String: the only difference is the invariant.
str is NOT a simple array of bytes. str contains characters that support the full Unicode range. To store a Unicode character, one needs at least 3 bytes. Many encodings exist, some of which try to maintain some sort of backward compatibility with good 'ole ASCII, like UTF-8. There is a format conversion from bytes to string and back, which converts e.g. UTF-8 encoded binary data into the Unicode representation used by Rust internally. I don't know for sure, but I wouldn't be surprised if Unicode characters are represented simply as u32 inside Rust.
@@TheEvertw str can be safely transmuted to [u8] and back (but it's UB if the sequence of bytes is not valid UTF8). char is indeed 4 bytes though.
@@Turalcar True, but as the different type implies, they are fundamentally different things. [u8] is an array of bytes that are an UTF-8-encoded representation of the str. That means it knows to skip the unused parts of an u32 Char, and that s.len() can be smaller than a.len().
@@TheEvertw They are different types solely due to invariants
@@TheEvertw Also, s.len() is length in bytes
My account has NaN money.
My account is an imaginary number
My account balance is of the i8 type.
Speed: null
Altitude: undefined
Inclination: NaN
Fuel: null
Oxidizer: undefined
Status: OK
@@Gruak7 This is sad.
Well NaN might be plural, so it has NaN moneys - I **think**?
Very interesting video! To be honest I have been programming like that since around two decades or so in C++ but there you are more limited by the type system how much you can enforce certain invariants. Great to see the power and flexibility of Rust's type system!
checked_sub is also useful to make that withdraw function.
Very nice, this feel a lot like "opaque types"
Didn't know generics can be used like this in structs 😮
Thank you!
Your videos are always great, this in particular is useful for other languages as well. Good work!
Bohdan, thank you for the new video. A very interesting point was made in the second part regarding Struct - it looks quite intriguing. I have only two comments: 1) If it's merely a trick example, no problem - it's interesting. However, if the goal was to demonstrate its usage in real applications, I'd like to advise newcomers that in real applications, it's more about meta-programming, and Roles should be at least enums. 2) An "empty" struct() doesn't cost anything, hence there's no need to use PhantomData; PhantomData serves another purpose, as demonstrated in the example.
I think the reason User isn't an enum is because all the data is the same so it's redundant to repeat it. And I think the reason why he didn't use a field with an enum is because the variant is known and doesn't need to be stored like an enum. That makes it less flexible than an enum because you have to know which one it is but it takes up less memory and means you can also give them all different implementations, like how only the editor even has the edit method. But yes, newcomers shouldn't use this.
You don't need an empty tuple struct like this "struct X();", it could also be a normal empty struct like this "struct X {}" or like how he did it like this "struct X;", which is similar. So the only thing you would have to do is remove the PhantomData wrapper.
Very helpful! Thank you.
Thanks! Best explanation as always
This can be achieved in other langs as well.
For example in C# we can use records for type safe struct data and so the same what we are doing it here.
Really liked this video, would love to hear more about good practice in rust.
Nice, I would love to see more videos about type-driven design
> bank account cannot be less than zero
damn I wish
Whoa, I bet that change from i32 to u32 makes the code riskier financially.
yeah, I think it wasn't the best example
At least rust has runtime checks to prevent overflow right?
Yup I ussually get good tit bits from creators like LGR - he does a great job of condensing ideas and digesting new features. But i32 to u32. I feel like I could make 4.3billion on quite easily ;)... It's very dangerous
i see, that it could make calculations not straightforward, as you cannot use + and - without implementing custom `add` and `sub` methods.
and you cannot have an account with debt.
but why it makes code riskier? i did not get.
@@s1v7 I know in C/C++ if you subtract 1 from 0 that's a unsigned int it wraps around to the largest possible number but in Rust I'm sure it simply panics saying that's not allowed unless I'm missing something.
Thank you, Bogdan. ✨🌞
Thanks for your high quality video !
Excellent
great vid!
07:18 => yet I would very much like to see a follow-up vid regarding this `PhantomData` what its uses are (beyond the Rust book if there is info available) and what did you mean exactly with "... to avoid unnecessary allocation."?
Look up "Improve your Rust APIs with the type state pattern" on this channel
@@KPidS nah... didn't get into that...I've only gotten lucky in the Rust Nomicon docs
Awesome video man ❤
really great video. Rusts type system is so beautiful. Can I cask how did you make the video? very beautiful animations.
Wow, great video! Thank you
Great video. Thanks!
We can also use a couple of other methods in some languages: 1. Design by contract 2. Inductive types
Very nice video.
Where is that style guide from?
Thank you for the video. Most of these principles can be used in most languages, it's a shame that they aren't popular amongst other developers.
One, for me rust is web dev is so interesting topic. I've just started rust but I have strong background on web development with dynamic typing language. We use validators mostly and a lot of tests to ensure everything is fine.
and so it will stay
when refinement types in rust?
Do more videos like this please, I was currently in the process of transitioning to this pattern and I found this very helpful.
6:15 When you need to parse a struct from string you should implement a FromStr trait instead. If you impelement FromStr, you can then do string.parse::(), like you can do with numbers string.parse::(). Similar to how you should implement From trait for conversion between other types, because you also get Into for free.
There'a also `AsRef` - the question is whether it worth using them in videos meant for not-yet-rustaceans, as it adds complexity of the trait idiom, arguably obfuscating the main idea
Also FromStr imply unnecessary allocation for copying the input string which is better to be consumed to also avoid the use afterwards by mistake, so I'd argue for just `fn new(s: String) -> Result`
Thank you for this great tutorial. I noticed one thing in your code. After switching to the Email and Password structs, you call the parse methods (6:30) but do not return a BadRequest anymore. What happens when Email::parse fails? I have no experience with rust and wonder what information the email and password structs actually contain when the validation failed.
Question: why do you use PhantomData on a zero sized struct? I thought the optimizer would not waste padding on a type that has no size.
I love Rust, and I really like this channel, but I actually think these patterns are pretty easy to implement in other languages too. I don't see Rust having a particularly big advantage where these patterns are concerned. Most people who have learned about DDD and some iteration of "clean" architecture will have likely seen how these patterns can be implemented in their language.
Why you didn't use the FromStr trait?
@letsgetrusty (aka Bohdon) how about a video with details on using the From,TryFrom and AsRef traits.
Awesome video!
the fact that the first thing said in this video is "this code is a pile of sh-[BLEEP]" cracked me up
I was already applying this concept with Domain Driven Design and value objects, however with rust it is much more powerful than in Typescript. I am in love with this language.
Can you make an updated rust best practices videos 📹 🙏
This can be implemented in any language, but Rust makes it so simple!
This concept/pattern is also more commonly known as "making impossible states impossible" (to represent so to speak).
Careful that the rust foundation doesn't get pissy bout that thumbnail
Life is too short to play it safe ;)
Cool, I had never seen an use case phantom data before
You’re right, it’s not often used. I’ve seen it used mainly in embedded systems contexts, such as hardware drivers.
Same thing, but I advise not to use it like in the example, because the default structure, if empty, does not consume memory at all. And PhantomData exists for absolutely other purpose.
I think the second half on the newtype pattern and parse don’t validate is much stronger than the opening using unsigned arithmetic. Having an always positive bank balance is not realistic, overdrafts happen, and just happens to align with an available number type. Adding parsing functions is much more flexible and future-proof. The validity of data is a separate concern from its representation.
It's just an example. If it's realistic or not doesn't matter. And he specifically said no overdrafts because it's to demonstrate a point.
Awesome!
More use default parse etc.
Дякую, Богдане. Корисно!
A big problem i have is coditional structs, like if type is `declined` that there is an `error` and if it's `accepted` that there is a `message`. This is possible trough enums, yes, but not so when receiving a json response from a 3rd party API.
Resut
@@christopher8641 So can I do Result ? So it will try all of them first. That would be great 🤣
@@aaaronme that is a sum type. As in, the type is the sum of all possible types. You should use an enum as the T in Result . Each enum variant is a newtype wrapper around a possible response from the API you are calling. I believe you would use an #[serde(untagged)] on the enum so serde tries all possible variants
@@christopher8641 Do you have a link for a documentation about this? I have actually been struggling with inconsistent API responses and currently just resulted in making almost every field Optional and one time I am trying to serialize it to StructA, if it fails, I know it's StructB,
meanwhile javascript devs: wait types exist?
Ok, I just have 3 lines in my most recent project that are exactly like 0:07. Let's see how to fix it.
Oh I use arch btw
Lmao, nice start of the video.
Tbf I've heard this 1000 times before.
How about something really underrepresented like combinators.
Sounds like this type-driven development will require detailed, ample documentation so everyone knows the invariants enforced and baked in.
Or else it might be hell to work with.
Never mind. The documentation is simply a design pattern.
Awesome
what happend to golangdojo
this is exactly what im looking for bro. you're mother fucking AWESOME
to be fair. using just strings in 99.99% cases is fine.
This is basically DDD
Best video ever
The bank example is really bad. And just about all the other examples can be made with untyped languages using parsing etc.
prase
"powerful software design methodology" -> basic if condition. Dude wtf?
No that wasn't the only thing, it also shows that you don't need to overuse if conditions if you can properly use the types, for example in the vid, he uses unsigned int which eliminates the to put an if condition
Smells like Java
"promo sm"
By the way I actually program in Rust using Vim on Arch.
I don't really see the difference with OOP. This is not special to Rust type system
You can do the basics of this with languages like python or C++ as well. But rust can do more. Especially when you start leveraging the trait system as well, to make the type system enforce invariants on generics, etc.
,o,
potato :D
Why not use `TryFrom` trait?
To simplify a bit User, User can simply be replaced with type UserEditor, UserViewer, etc. The use of generics is just type masturbation here.
Type masturbation...
What an interesting term 😆
your User and each its variant are likely to have an implementation. so you'd create a common implementation for any type T in User and then add some more traits for User, User and whatever role you'll decide to add later
@@sunofabeach9424 Wrapping shared fields in BaseUser struct that's shared between all variants is very simple
@@sunofabeach9424 Not saying it does not have its uses, I would recommend simpler composition for a start, because rarely the role carries no additional data
There's a convention to use `AuthError::Validation` instead of `AuthError::ValidationError`
This code could be way better like this. th-cam.com/video/NDIU1GSBrVI/w-d-xo.html
fn withdraw(&mut self, withdraw_amount: u32) -> Result {
self.amount = self.amount.checked_sub(withdraw_amount).ok_or("not enough money!".to_string())?;
Ok(self.amount)
}
the checked_sub method on u32 checks for overflow and returns None which we can convert to a result with .ok_or()
otherwise good video :)
Yes could definitely be improved :)