Another banger from Oliver Jumpertz 🔥 Love the way you're visually explaining this. For someone like me (completely unexperienced in Rust), this is super helpful.
Nice video. One nice alternative to unwrap/expect is the let-else pattern. Example: let Some(value) = some_option else { return /* or break/continue/etc */}. Thereafter "value" holds the unwrapped value, and you've handled the error case.
Great video! I even learned a few new things regarding implementing your own error type. Instead of expect I would use ".map_err(...)?", ".ok_or(...)?" or even just "?" in parts of code where I know they shouldn't panic regardless of what the programmer does (api calls for example). In parts of code where I definitely need to stop the whole process (say, a database connection error or missing .env variables) I just use ".expect(...)" as shown
Yep! The "really" best way to express the problem in Rust is probably this implementation (as already shown and discussed in other comments): fn try_from(input: &str) -> Result { let mut split = input.split('=').map(str::trim).map(str::to_owned) match (split.next(), split.next(), split.next()) { (Some(key), Some(value), None) => Ok(Self{ key: key.to_owned(), value: value.to_owned() }), _ => Err(ParsingError::MalformedRecord(input.into())), } } I generally agree that I'd do it this way. In this case, I just had to make an easy-enough example to showcase the problem, which also does not turn away complete Rust newbies. So in general, in this example, you CAN "implement the problem away" by using more advanced language features. :)
@@oliverjumpertzme That's a creative way of using pattern matching. Didn't even cross my mind. I guess the only downside is it's allocating a tuple. Might use that in my parsing implementations from now on actually Also, I agree, sometimes just using "match" is better. Especially if you need to treat errors as "warnings" and use a control statement such as "continue;"
or more simply: ``` fn try_from( input: &str ) -> Result { let mut split = input.split( '=' ); match (split.next(), split.next(), split.next()) { (Some(key), Some(value), None) => Ok( Self{ key: key.trim().to_owned(), value: value.trim().to_owned() }), _ => Err(ParsingError::MalformedRecord( input.into() )), } }``` Good video still, your main point is valid and well explained.
I love Rust, and that's in no small part down to its community. I would not have even considered doing it this way before seeing this comment, and yet it becomes so obvious (and so expressive) that I immediately prefer it. Thank you.
The sound effects are a bit excessive and more of a distraction than anything else. Is there any chance we could get an upload without all the artificial sounds of writing, typing, etc.?
@@oliverjumpertzme I appreciate the response. It could just be that the sounds seem too loud compared to your voice (at least with earbuds). Either way, thanks for the content 🙂
@@oliverjumpertzme I agree with @lukez8352 on that the sounds was little too loud and I could only hear the effect in my left ear. I suggest you dont use stereo for them. I would keep the effects tho, they are great addition. Thansk for the video!
Super helpful! Thanks Oliver I have a question though, I use axum framework at work and almost all of my endpoint handler functions return the error type in Response, I use the Response::builder() function to usually convert the error response into my desired HTTP response like 400, 403 or 500 etc My question to you is, as per the docs it has the unwrap() function at the end, how can I possibly handle the errors in this case, especially considering the fact that I want to do it gracefully and not just use .expect() cause it looks like a shortcut What is the general convention for handling the unwrap() which are in the docs for all the libraries we use?
Hey, thanks a lot! 💛 Regarding your question: I think we need to distinguish between errors and bugs here ( once again :) ). The question, thus, is: Is your response not building correctly a real bug or something you should expect and handle? First of all, I'd make sure that your route handler either returns a Result or something like Result (because the latter can easily be converted to a valid response by axum's buit-in converters). If you return a non-dynamic response, i.e. no dynamic contents or just little, mostly statically defined in your source code, I'd still use expect because if that Response doesn't build, that's a real bug. An example of the sole return statement of a handler function: Ok(Response::builder() .status(StatusCode::TEMPORARY_REDIRECT) .header("Location", "example.com/redirect-url".into()) .body(Body::empty()) .expect("This response should always be constructable")) There is nothing really dynamic in this response and building that should not fail if you do everything correctly. Or let's say: If something fails, that's a bug. If you have a highly dynamic response that you can't really control, I'd map to an internal server error, like so: Ok(Response::builder() . status(StatusCode::OK) .header("Cache-Control", DEFAULT_CACHE_CONTROL_HEADER_VALUE) .body(json!(response_object).to_string()) // given some Error implements IntoResponse, so axum can convert it correctly .map_err(|_| SomeError::INTERNAL_PROCESSING_ERROR)?) or Ok(Response::builder() . status(StatusCode::OK) .header("Cache-Control", DEFAULT_CACHE_CONTROL_HEADER_VALUE) .body(json!(response_object).to_string()) // given your return type is Result .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Processing failed".into())?) But I think to have understood that you do something like this?: get_optional_value() .ok_or_else(|_| Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty())...???) If that's the case, I think the same rules apply. If it is completely static, use expect. Your hard-coded response not building correctly is a bug: get_optional_value() .ok_or_else(|_| Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).expect("Oops, bug!")) If it is highly dynamic, you have a problem because your return type basically needs a Response but building that response is fallible, which would lead to an infinite loop of map_err's with question marks (I hope you understand what I mean). So my suggestion: Either make your peace with expect, or change the return type of your handler function to something more flexible like: Result or Result, which gives you a lot more flexibility to define your errors. Alternatively, create an error type that implements IntoResponse (which also carries the risk of having to build a fallible Response again that...yea...might end in the same infinite loop of error handling). Does this help, or shall we go over this in more detail? :)
Yes, that works, and it's completely valid, probably even better than this example, but tbh, I had to find an example where it really makes sense to use something like unwrap (and these examples really exist), which is why I went with this way of doing it. 😁
I would write the code in a way to avoid having situations where I know more than the compiler. Rust is a language with a strong type system. Almost all states can be expressed in it. A Taking your example I would have removed the check for contains and the check for size < 2. These are unnecessary as the vector api can already cover these. Calling get with 0, 1 and 2 and reacting to the options using match or let else statements covers the error states and unwraps the content without unwrap or expect.
It depends. We're not using a sophisticated parser here. We need at least the length check to enforce the notation "key=value". Otherwise "key=value=value" would be interpreted as: Record { key: "key", value: "value", } while completely ignoring that the Record, in our example with a strict notation, was indeed malformed. Replacing the existing length check with something based on calling .get(2) and then reacting based on the presence of a value like: // this is btw what Clippy will tell you to replace if let Some(_) = split.get(2) with if split.get(2).is_some() { return Err(ParsingError::MalformedRecord(value.into()); } is also no good alternative, imho. Yes, the vector API can do a lot, thanks to its Options, but the question always remains is: Is that code necessarily better? Imho, not, but how well someone likes a certain style of programming is subjective! If we already use one check, the other check also doesn't hurt and even reveals intention and serves as documentation. :)
@@oliverjumpertzme i disagree. It is not necessarily easier to read. But it is safer code as is doesn't rely on logical but not typed promises. Your version checks the conditions twice but panics if they don't uphold on the second check. In my opinion this is inferior to checking and getting in 1 go. Coupling extraction with handling of the lack of data is an innovation that your code doesn't use.
I think we can agree to disagree. I implemented your version, or what I assume to be your version, and it led to (imho) far less readable code (and a lot more of it). There is of course a way to convert from an Option to a Result, which allows us to do something like: let key = split.first().ok_or_else(|| ParsingError::MalformedRecord(value.into()))?; let value = split.get(1).ok_or_else(|| ParsingError::MalformedRecord(value.into()))?; in which I'd agree with you for this to be a better version. But it still leaves us with the length case of strict format enforcement, and I still advocate for: if split.len() > 2 { return Err(ParsingError::MalformedRecord(value.into()); } instead of adding something like either at the beginning (where it would be best suited) or after the other statements: if split.get(2).is_some() { return ...; } But once again, tastes are subjective. :)
I thought about this again, and I now believe that what you meant is code like this (also, someone else already posted it here): fn try_from(input: &str) -> Result { let mut split = input.split('=').map(str::trim).map(str::to_owned) match (split.next(), split.next(), split.next()) { (Some(key), Some(value), None) => Ok(Self{ key: key.to_owned(), value: value.to_owned() }), _ => Err(ParsingError::MalformedRecord(input.into())), } } This code is indeed superior because it completely avoids a panic at any time without changing the logic and completely stays within the warmth of Rust's advanced type system. I have even considered using it, but I have not used it in the video for two reasons: 1. It did not suit my case. You might not believe it, but there are still situations in which you can't optimize code to not use some form of unwrap. I have code of an RFC-9111-compliant parser in front of me that uses PEST to do the parsing of header strings. In there, I really have the situation that I cannot use advanced pattern matching because the absence of some AST nodes would actually mean that the PEG is broken. We can now argue that there MIGHT still be a way, but sometimes, you reach that point. We also have to take into account that sometimes, we all don't know the perfect solution for a problem, and for these cases, the main points of my video (imho) apply: Use expect, mark bugs explicitly. :) 2. I try to make my videos accessible even to Rust newbies, and the advanced pattern matching capabilities of Rust are often very difficult for them to grasp. I would probably have needed several additional minutes to explain what happens and why that is a great idea, while the main point of the video was a whole different. I hope this helps you better understand my point of view. :)
Do you mean: if split.len() != 2 { return Err(ParsingError::MalformedRecord(value.into())); } Yea, that also works. :) There are several ways to do it. I chose this approach to illustrate the overall thing better. Optimization is a good thing, but it often costs additional explanation. View this just as example code to prove a point.
The code example has a lot of duplicated works. If you both `value.contains('=')` and `value.split('=')` with matching the result of `value.splitn(2, '=')`, all panics would be eliminated.
I am well aware of that, but that was not the main point of the video, which is why I used an example everyone would understand, no matter their experience in Rust. ;) And no, multiple equals signs never panic. They just return an err because for this particular example, we only allow key=value. If you really want equals signs to be allowed in either key or value, I’d actually involve quotes in the grammar, like most sane languages and parsers do.
Another banger from Oliver Jumpertz 🔥 Love the way you're visually explaining this. For someone like me (completely unexperienced in Rust), this is super helpful.
Thank you, Simon! And remember, you're my inspiration! 💛
Nice video. One nice alternative to unwrap/expect is the let-else pattern. Example: let Some(value) = some_option else { return /* or break/continue/etc */}. Thereafter "value" holds the unwrapped value, and you've handled the error case.
Excellent as always. If you are ever in St Louis we would love to have you at the STL Rust Meetup.
Hey, thanks a lot! Oh, if I ever make it to St. Louis, I'll remember that! 💛
Great video and I learning a few things and helped me remember other things I was almost forgetting about. Thanks!
Awesome! 💛
Me who uses unwrap_unchecked and pointer manipulation to flatten a vector of arrays
If you REALLY know what you're doing…you're free to. 😂
You're the best Rust RUclipsrs. Thanks! :)
Awwww, I am sure I still have a lot to learn, but this makes my day…so thank you! 💛
Great video! I even learned a few new things regarding implementing your own error type.
Instead of expect I would use ".map_err(...)?", ".ok_or(...)?" or even just "?" in parts of code where I know they shouldn't panic regardless of what the programmer does (api calls for example). In parts of code where I definitely need to stop the whole process (say, a database connection error or missing .env variables) I just use ".expect(...)" as shown
Yep!
The "really" best way to express the problem in Rust is probably this implementation (as already shown and discussed in other comments):
fn try_from(input: &str) -> Result {
let mut split = input.split('=').map(str::trim).map(str::to_owned)
match (split.next(), split.next(), split.next()) {
(Some(key), Some(value), None) => Ok(Self{
key: key.to_owned(),
value: value.to_owned()
}),
_ => Err(ParsingError::MalformedRecord(input.into())),
}
}
I generally agree that I'd do it this way.
In this case, I just had to make an easy-enough example to showcase the problem, which also does not turn away complete Rust newbies. So in general, in this example, you CAN "implement the problem away" by using more advanced language features. :)
@@oliverjumpertzme That's a creative way of using pattern matching. Didn't even cross my mind. I guess the only downside is it's allocating a tuple. Might use that in my parsing implementations from now on actually
Also, I agree, sometimes just using "match" is better. Especially if you need to treat errors as "warnings" and use a control statement such as "continue;"
Great video! Learned something new about unwrap() / expect()
p.s is the theme you are using the Night Owl theme?
Hey, glad to hear that!
Yes, it is. ☺️
There are situations where you have constraints where unwrap is save as you have guaranteed ok or some value
Due to API constraints. Except is still nicer tho
or more simply:
``` fn try_from( input: &str ) -> Result {
let mut split = input.split( '=' );
match (split.next(), split.next(), split.next()) {
(Some(key), Some(value), None) => Ok( Self{
key: key.trim().to_owned(),
value: value.trim().to_owned()
}),
_ => Err(ParsingError::MalformedRecord( input.into() )),
}
}```
Good video still, your main point is valid and well explained.
Also valid ☺️👍🏻
Yes! I reckon this is the best way.
Why forbid the value to contain '='? Just use `splitn(2, '=')`.
I love Rust, and that's in no small part down to its community. I would not have even considered doing it this way before seeing this comment, and yet it becomes so obvious (and so expressive) that I immediately prefer it. Thank you.
Thanks for sharing. I reall learnt something new today.
p.s: what do you use to animate your code to make it look like you'r typing?
Thanks! I use snappify. :)
The sound effects are a bit excessive and more of a distraction than anything else. Is there any chance we could get an upload without all the artificial sounds of writing, typing, etc.?
Hey, sorry to hear that. Let me think about that. To me, it feels liveless without them, but I can understand you. ☺️
@@oliverjumpertzme I appreciate the response. It could just be that the sounds seem too loud compared to your voice (at least with earbuds). Either way, thanks for the content 🙂
@@lukez8352 that's awesome feedback, thank you! I am still learning audio engineering. Maybe I need to tone it down further. Thank you again! 💛
@@oliverjumpertzme I agree with @lukez8352 on that the sounds was little too loud and I could only hear the effect in my left ear. I suggest you dont use stereo for them.
I would keep the effects tho, they are great addition.
Thansk for the video!
Please also consider making these sounds play on both channels. Currently, there's only a left channel and it's not pleasurable with headphones.
Super helpful! Thanks Oliver
I have a question though, I use axum framework at work and almost all of my endpoint handler functions return the error type in Response,
I use the Response::builder() function to usually convert the error response into my desired HTTP response like 400, 403 or 500 etc
My question to you is, as per the docs it has the unwrap() function at the end, how can I possibly handle the errors in this case, especially considering the fact that I want to do it gracefully and not just use .expect() cause it looks like a shortcut
What is the general convention for handling the unwrap() which are in the docs for all the libraries we use?
Hey, thanks a lot! 💛
Regarding your question:
I think we need to distinguish between errors and bugs here ( once again :) ).
The question, thus, is: Is your response not building correctly a real bug or something you should expect and handle?
First of all, I'd make sure that your route handler either returns a Result or something like Result (because the latter can easily be converted to a valid response by axum's buit-in converters).
If you return a non-dynamic response, i.e. no dynamic contents or just little, mostly statically defined in your source code, I'd still use expect because if that Response doesn't build, that's a real bug.
An example of the sole return statement of a handler function:
Ok(Response::builder()
.status(StatusCode::TEMPORARY_REDIRECT)
.header("Location", "example.com/redirect-url".into())
.body(Body::empty())
.expect("This response should always be constructable"))
There is nothing really dynamic in this response and building that should not fail if you do everything correctly. Or let's say: If something fails, that's a bug.
If you have a highly dynamic response that you can't really control, I'd map to an internal server error, like so:
Ok(Response::builder()
. status(StatusCode::OK)
.header("Cache-Control", DEFAULT_CACHE_CONTROL_HEADER_VALUE)
.body(json!(response_object).to_string())
// given some Error implements IntoResponse, so axum can convert it correctly
.map_err(|_| SomeError::INTERNAL_PROCESSING_ERROR)?)
or
Ok(Response::builder()
. status(StatusCode::OK)
.header("Cache-Control", DEFAULT_CACHE_CONTROL_HEADER_VALUE)
.body(json!(response_object).to_string())
// given your return type is Result
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "Processing failed".into())?)
But I think to have understood that you do something like this?:
get_optional_value()
.ok_or_else(|_| Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty())...???)
If that's the case, I think the same rules apply. If it is completely static, use expect. Your hard-coded response not building correctly is a bug:
get_optional_value()
.ok_or_else(|_| Response::builder().status(StatusCode::NOT_FOUND).body(Body::empty()).expect("Oops, bug!"))
If it is highly dynamic, you have a problem because your return type basically needs a Response but building that response is fallible, which would lead to an infinite loop of map_err's with question marks (I hope you understand what I mean).
So my suggestion:
Either make your peace with expect, or change the return type of your handler function to something more flexible like: Result or Result, which gives you a lot more flexibility to define your errors.
Alternatively, create an error type that implements IntoResponse (which also carries the risk of having to build a fallible Response again that...yea...might end in the same infinite loop of error handling).
Does this help, or shall we go over this in more detail? :)
Nice presentation!
I'd have used pattern matching to select the key and the value and handle all other conditions at once, though.
Yes, that works, and it's completely valid, probably even better than this example, but tbh, I had to find an example where it really makes sense to use something like unwrap (and these examples really exist), which is why I went with this way of doing it. 😁
Great video. Thanks
Thanks for watching and the feedback 💛🙏🏻
I would write the code in a way to avoid having situations where I know more than the compiler. Rust is a language with a strong type system. Almost all states can be expressed in it.
A
Taking your example I would have removed the check for contains and the check for size < 2. These are unnecessary as the vector api can already cover these. Calling get with 0, 1 and 2 and reacting to the options using match or let else statements covers the error states and unwraps the content without unwrap or expect.
It depends. We're not using a sophisticated parser here.
We need at least the length check to enforce the notation "key=value". Otherwise "key=value=value" would be interpreted as:
Record {
key: "key",
value: "value",
}
while completely ignoring that the Record, in our example with a strict notation, was indeed malformed.
Replacing the existing length check with something based on calling .get(2) and then reacting based on the presence of a value like:
// this is btw what Clippy will tell you to replace if let Some(_) = split.get(2) with
if split.get(2).is_some() {
return Err(ParsingError::MalformedRecord(value.into());
}
is also no good alternative, imho.
Yes, the vector API can do a lot, thanks to its Options, but the question always remains is: Is that code necessarily better? Imho, not, but how well someone likes a certain style of programming is subjective!
If we already use one check, the other check also doesn't hurt and even reveals intention and serves as documentation. :)
@@oliverjumpertzme i disagree. It is not necessarily easier to read. But it is safer code as is doesn't rely on logical but not typed promises. Your version checks the conditions twice but panics if they don't uphold on the second check.
In my opinion this is inferior to checking and getting in 1 go. Coupling extraction with handling of the lack of data is an innovation that your code doesn't use.
I think we can agree to disagree. I implemented your version, or what I assume to be your version, and it led to (imho) far less readable code (and a lot more of it).
There is of course a way to convert from an Option to a Result, which allows us to do something like:
let key = split.first().ok_or_else(|| ParsingError::MalformedRecord(value.into()))?;
let value = split.get(1).ok_or_else(|| ParsingError::MalformedRecord(value.into()))?;
in which I'd agree with you for this to be a better version.
But it still leaves us with the length case of strict format enforcement, and I still advocate for:
if split.len() > 2 {
return Err(ParsingError::MalformedRecord(value.into());
}
instead of adding something like either at the beginning (where it would be best suited) or after the other statements:
if split.get(2).is_some() {
return ...;
}
But once again, tastes are subjective. :)
I thought about this again, and I now believe that what you meant is code like this (also, someone else already posted it here):
fn try_from(input: &str) -> Result {
let mut split = input.split('=').map(str::trim).map(str::to_owned)
match (split.next(), split.next(), split.next()) {
(Some(key), Some(value), None) => Ok(Self{
key: key.to_owned(),
value: value.to_owned()
}),
_ => Err(ParsingError::MalformedRecord(input.into())),
}
}
This code is indeed superior because it completely avoids a panic at any time without changing the logic and completely stays within the warmth of Rust's advanced type system.
I have even considered using it, but I have not used it in the video for two reasons:
1. It did not suit my case. You might not believe it, but there are still situations in which you can't optimize code to not use some form of unwrap. I have code of an RFC-9111-compliant parser in front of me that uses PEST to do the parsing of header strings. In there, I really have the situation that I cannot use advanced pattern matching because the absence of some AST nodes would actually mean that the PEG is broken. We can now argue that there MIGHT still be a way, but sometimes, you reach that point. We also have to take into account that sometimes, we all don't know the perfect solution for a problem, and for these cases, the main points of my video (imho) apply: Use expect, mark bugs explicitly. :)
2. I try to make my videos accessible even to Rust newbies, and the advanced pattern matching capabilities of Rust are often very difficult for them to grasp. I would probably have needed several additional minutes to explain what happens and why that is a great idea, while the main point of the video was a whole different.
I hope this helps you better understand my point of view. :)
AFAIK, panic only terminate the current thread, other threads keep running if the crashed one is not the main one.
Yes. As shown. But for many programs, even a secondary thread panicking can bring the whole system down. ☺️
Would it not be better in the parser example to make the error condition `!=`?
Do you mean:
if split.len() != 2 {
return Err(ParsingError::MalformedRecord(value.into()));
}
Yea, that also works. :) There are several ways to do it. I chose this approach to illustrate the overall thing better. Optimization is a good thing, but it often costs additional explanation. View this just as example code to prove a point.
The code example has a lot of duplicated works. If you both `value.contains('=')` and `value.split('=')` with matching the result of `value.splitn(2, '=')`, all panics would be eliminated.
I am well aware of that, but that was not the main point of the video, which is why I used an example everyone would understand, no matter their experience in Rust. ;)
And no, multiple equals signs never panic. They just return an err because for this particular example, we only allow key=value.
If you really want equals signs to be allowed in either key or value, I’d actually involve quotes in the grammar, like most sane languages and parsers do.
Will it fail if it has "key=" ?
No, it would result in a:
Record {
key: "key",
value: ""
}
:)
The guy forgot to show how to refactor the code to make unwrap unnecesary
”The guy” had a point to make which was not to optimize each and every bit of some code. 😉
.unwrap() is for the weak. Real men handle errors :D
Yes! 😁💪🏻
When there is no recovery possible, unwrap is ok
No, real men use unwrap_unchecked()
more Rust videos, bro.
On it! 😁