You're probably misusing unwrap in Rust...let's fix that

Поделиться
HTML-код
  • Опубликовано: 14 янв 2025

Комментарии • 67

  • @SimonHoiberg
    @SimonHoiberg 3 месяца назад +13

    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.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад +1

      Thank you, Simon! And remember, you're my inspiration! 💛

  • @peterryszkiewicz4354
    @peterryszkiewicz4354 3 месяца назад +2

    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.

  • @ntippy
    @ntippy 3 месяца назад +1

    Excellent as always. If you are ever in St Louis we would love to have you at the STL Rust Meetup.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      Hey, thanks a lot! Oh, if I ever make it to St. Louis, I'll remember that! 💛

  • @lcssbr
    @lcssbr 3 месяца назад +1

    Great video and I learning a few things and helped me remember other things I was almost forgetting about. Thanks!

  • @Iuigi_t
    @Iuigi_t 3 месяца назад +7

    Me who uses unwrap_unchecked and pointer manipulation to flatten a vector of arrays

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      If you REALLY know what you're doing…you're free to. 😂

  • @OfficialViper
    @OfficialViper 3 месяца назад +1

    You're the best Rust RUclipsrs. Thanks! :)

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад +1

      Awwww, I am sure I still have a lot to learn, but this makes my day…so thank you! 💛

  • @CodeVault
    @CodeVault 3 месяца назад +1

    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

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      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. :)

    • @CodeVault
      @CodeVault 3 месяца назад +1

      @@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;"

  • @abdulrahmanmohamed8298
    @abdulrahmanmohamed8298 3 месяца назад +2

    Great video! Learned something new about unwrap() / expect()
    p.s is the theme you are using the Night Owl theme?

  • @matthiskalble3621
    @matthiskalble3621 3 месяца назад

    There are situations where you have constraints where unwrap is save as you have guaranteed ok or some value

    • @matthiskalble3621
      @matthiskalble3621 3 месяца назад

      Due to API constraints. Except is still nicer tho

  • @catsolstice
    @catsolstice 3 месяца назад +12

    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.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      Also valid ☺️👍🏻

    • @Darfk
      @Darfk 3 месяца назад +2

      Yes! I reckon this is the best way.

    • @khai96x
      @khai96x 3 месяца назад

      Why forbid the value to contain '='? Just use `splitn(2, '=')`.

    • @alexclark6777
      @alexclark6777 3 месяца назад +1

      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.

  • @eligbuefelix7988
    @eligbuefelix7988 Месяц назад +1

    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?

  • @lukez8352
    @lukez8352 3 месяца назад +15

    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
      @oliverjumpertzme  3 месяца назад +1

      Hey, sorry to hear that. Let me think about that. To me, it feels liveless without them, but I can understand you. ☺️

    • @lukez8352
      @lukez8352 3 месяца назад +4

      @@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
      @oliverjumpertzme  3 месяца назад +1

      @@lukez8352 that's awesome feedback, thank you! I am still learning audio engineering. Maybe I need to tone it down further. Thank you again! 💛

    • @Pyro2352
      @Pyro2352 3 месяца назад

      @@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!

    • @maxch3
      @maxch3 3 месяца назад +3

      Please also consider making these sounds play on both channels. Currently, there's only a left channel and it's not pleasurable with headphones.

  • @aspiring_millionaire
    @aspiring_millionaire 3 месяца назад +1

    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?

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      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? :)

  • @DuniC0
    @DuniC0 3 месяца назад +1

    Nice presentation!
    I'd have used pattern matching to select the key and the value and handle all other conditions at once, though.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      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. 😁

  • @miriamramstudio3982
    @miriamramstudio3982 3 месяца назад

    Great video. Thanks

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      Thanks for watching and the feedback 💛🙏🏻

  • @redcrafterlppa303
    @redcrafterlppa303 3 месяца назад +2

    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.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад +1

      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. :)

    • @redcrafterlppa303
      @redcrafterlppa303 3 месяца назад

      @@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.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад +1

      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. :)

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      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. :)

  • @FrancescoBochicchio-ly6du
    @FrancescoBochicchio-ly6du 3 месяца назад

    AFAIK, panic only terminate the current thread, other threads keep running if the crashed one is not the main one.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      Yes. As shown. But for many programs, even a secondary thread panicking can bring the whole system down. ☺️

  • @sunilpaul6891
    @sunilpaul6891 3 месяца назад +1

    Would it not be better in the parser example to make the error condition `!=`?

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      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.

  • @khai96x
    @khai96x 3 месяца назад

    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.

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      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.

  • @RomanAvdeevX
    @RomanAvdeevX 3 месяца назад +1

    Will it fail if it has "key=" ?

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад

      No, it would result in a:
      Record {
      key: "key",
      value: ""
      }
      :)

  • @vasiliigulevich9202
    @vasiliigulevich9202 3 месяца назад +1

    The guy forgot to show how to refactor the code to make unwrap unnecesary

    • @oliverjumpertzme
      @oliverjumpertzme  3 месяца назад +1

      ”The guy” had a point to make which was not to optimize each and every bit of some code. 😉

  • @qbasic16
    @qbasic16 3 месяца назад +5

    .unwrap() is for the weak. Real men handle errors :D

  • @ИванРагозин-я8я
    @ИванРагозин-я8я Месяц назад +1

    more Rust videos, bro.