@@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
@@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.
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.
@@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().
@@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)
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.
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
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.
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.
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`
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."?
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.
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.
7:16 I don't think the use of PhantomData is useful because UserRole are unit types with a size of zero (aka ZST), they only exists at compile time and not runtime.
I don't get how this ensures that the email is valid if it's an instance of Email. We could just do let email = Email(String::from("whatever")) and pass it around. There's no way to enforce going through Email::parse.
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.
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.
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.
@@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,
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.
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.
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!
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
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.
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 Not saying it does not have its uses, I would recommend simpler composition for a start, because rarely the role carries no additional data
This code could be way better like this. ua-cam.com/video/NDIU1GSBrVI/v-deo.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 :)
📝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?
@@antifa_communist 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
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
6:32 prase -> parse
"parse, don't prase"
Good catch
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)
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.
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
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**?
This jumped on the new level in terms of the video effects and presentation. Wonderful!
We can also use a couple of other methods in some languages: 1. Design by contract 2. Inductive types
I love these kinds of videos, please make more videos with logical errors and it's fix with code examples
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.
Very nice, this feel a lot like "opaque types"
> bank account cannot be less than zero
damn I wish
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.
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`
Didn't know generics can be used like this in structs 😮
Thank you!
the fact that the first thing said in this video is "this code is a pile of sh-[BLEEP]" cracked me up
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
Well done 💯 The production value and editing on your videos continues to get better and better. This was a good one. It is appreciated.
checked_sub is also useful to make that withdraw function.
Yeah I was about to comment that. But maybe he wanted to make the code more imparative so that new devs can easily understand
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
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.
Technical stuff aside... this channel gives off a cult like feel. Listening to this presentation is like listening to a preacher.
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.
7:16 I don't think the use of PhantomData is useful because UserRole are unit types with a size of zero (aka ZST), they only exists at compile time and not runtime.
Where is that style guide from?
I don't get how this ensures that the email is valid if it's an instance of Email. We could just do let email = Email(String::from("whatever")) and pass it around. There's no way to enforce going through Email::parse.
how to create number where number cannot equal 42? which type?
Great video! 🙌 Would love more like this where you go through best practices with examples
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.
Nice, I would love to see more videos about type-driven design
Careful that the rust foundation doesn't get pissy bout that thumbnail
Life is too short to play it safe ;)
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.
Why you didn't use the FromStr trait?
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).
when refinement types in rust?
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,
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.
Your videos are always great, this in particular is useful for other languages as well. Good work!
Really liked this video, would love to hear more about good practice in rust.
@letsgetrusty (aka Bohdon) how about a video with details on using the From,TryFrom and AsRef traits.
Wow, it's a really cool concept
Very nice video.
Дякую, Богдане. Корисно!
Can you make an updated rust best practices videos 📹 🙏
Thanks! Best explanation as always
meanwhile javascript devs: wait types exist?
really great video. Rusts type system is so beautiful. Can I cask how did you make the video? very beautiful animations.
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.
Excellent
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!
Thanks for your high quality video !
There's no overflow check in deposit implementation. Its BUG
Ok, I just have 3 lines in my most recent project that are exactly like 0:07. Let's see how to fix it.
Wow, great video! Thank you
Great video. Thanks!
Very helpful! Thank you.
Thank you, Bogdan. ✨🌞
Awesome video!
Do more videos like this please, I was currently in the process of transitioning to this pattern and I found this very helpful.
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.
More use default parse etc.
Awesome video man ❤
Tbf I've heard this 1000 times before.
How about something really underrepresented like combinators.
i feel like you can do this even in C
what happend to golangdojo
"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
to be fair. using just strings in 99.99% cases is fine.
The bank example is really bad. And just about all the other examples can be made with untyped languages using parsing etc.
Oh I use arch btw
Lmao, nice start of the video.
By the way I actually program in Rust using Vim on Arch.
this is exactly what im looking for bro. you're mother fucking AWESOME
Awesome!
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.
and…. he didn't say it was exclusive to rust 😊
This is basically DDD
Awesome
Это просто гениально
Smells like Java
Best video ever
prase
"promo sm"
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
This code could be way better like this. ua-cam.com/video/NDIU1GSBrVI/v-deo.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 :)