0
jestdotty
42d

lol rust has no early return from a match statement

continue to skip rest of loop
break to exit a loop
return to exit a function

they put in let Ok(response) = request.send() else { return None };
then you can use response like normal after

but let's say I wanna know what the error was (Ok being a variant of Result::Ok or Result::Err, and the above allowing you to destructure and go on or exit early because can't destructure)

let response = match response.send() {
Ok(response) => response,
Err(err) => {
// log error to file or whatever
eprintln!("{err:#?}");
//????? HOW DO I BREAK OUT OF HERE
return None //whole function shits itself instead of just exiting match
}
}
//does some stuff with response

actually in my case the result will be wrapped in a Ok again so I'm not doing justice to explaining this problem, fux
but basically I need to exit the match without ending the function

come on, match is a loop. let me break, fuckers.

Comments
  • 2
    From my understanding / experience, match is a replacement for "switch-case". It is not a replacement for "for/while". Switch doesn't support return either, executing return will exit the whole method / function.

    So.. maybe.. rewrite your code?
  • 1
    I don't know Rust, but I suppose you shouldn't be able to break out of that case the way you are describing. If you did, the code below the match statement would try to work with response even though its value is undefined, so you always have to return something.
    This is unless having undefined variables is common in Rust and the code below would check for that, but in that case, why not do the stuff with Ok(response) directly in the match, as you already have that type check done in that context?
    Again, I'm not familiar with Rust, but I hope this makes at least a bit of sense :)
  • 2
    Break inside switch statements are just boilerplate code and a source of bugs when you forget them. I'm glad that new languages don't support them.
  • 1
    @cafecortado What you said bro. IMHO switch-case should only contain opening curly brace and closing curly brace. Using colon, line break, and break; statement to conclude it --> feels so archaic and smell of BASIC language. Unfortunately most languages still do this, for backward compatibility. Break; should be something you use to end an encasing prematurely, it shouldn't exist as the last statement on every cases.
  • 1
    match is not switch, the equivalent of a break from switch happens automatically every time. If you assign the match expression to a variable you need to either produce a value of the right type or break out of some block that contains the assignment so that the assignment never happens. This is all completely logical and necessary.
  • 1
    you can break out of any loop. you can't break WITH A VALUE from a loop that also has non-break exits such as a for or while, again, this is completely necessary for correctness. The difference between a break with and without value is also emphasized in the error message when you try to break with value from the wrong kind of loop.
  • 1
    There are also named blocks that you can target with breaks, this allows you to express any control flow that doesn't try to skip assignments.
  • 1
    @jestdotty No, theres no early "return" from within a match arm

    In your specific case you can either solve this with a nested pattern or by mapping the option:

    (or traditionally with a mut variable and an if, if youre feeling old school)

    // Using nested pattern

    Ok(match x {

    Thing::A => Some(12),

    Thing::B(Some(value)) => {

    Some(value * 36464)

    }

    Thing::B(None) => None,

    })

    // Mapping the option

    Ok(match x {

    Thing::A => Some(12),

    Thing::B(maybe) => {

    maybe.map(|v| v * 36464)

    }

    })
  • 1
    @jestdotty Or if you REALLY want the nested if, you can do it like this:

    Ok(match x {

    Thing::A => Some(12),

    Thing::B(value) => {

    if let Some(value) = value {

    Some(value * 36464)

    } else {

    None

    }

    }

    })
  • 0
    @jestdotty Not sure how your code looks like but there is probably a better approach than these complex matches

    What are you matching anyways? If it's just for "normal" error handling / detecting absent values you can use the try operator, which is a form of early return

    By default it's for the entire function call though (returns from the function), else you need to unstable try_blocks feature:

    https://play.rust-lang.org//...

    Of course when you're dealing with Results its a bit more complex than Option since the error types have to be compatible. The cool thing is, the try/? operator calls .into() on the error to try to make it compatible with the target error type of the function / try block, so if you are frequently turning a ParseError into RequestError, you can just implement From<ParseError> for RequestError instead of having to call .map_err() all over the place
  • 0
    @jestdotty Sure, im up for it
  • 1
    @jestdotty Something approximately like this should work (haven't tested since I can't compile it): https://play.rust-lang.org//...

    This relies heavily on the try operator which is awesome for error handling and the thiserror crate for defining an error enum. (you might also have to turn on the json feature of ureq)

    Apart from that the code looks pretty good, maybe some small nitpicks:

    * you use a lot of inline defined structs, there is nothing wrong with that but it does harm readibility a bit imo

    * I'm not too hot on making Request an enum and having send return an "untyped" Response, I would rather have concrete structs, but it's not that bad
  • 0
    @jestdotty You don't /have/ to wrap errors into enums, only if you want to programatically handle them. For very simple error handling check out the anyhow crate which gives you an opaque "Error" type which any other error can be converted into and which captures a stack trace, etc. Then you can still use the ? operator but not worry about defining your own error enum

    Trying to avoid dependencies makes sense but Rust has a lot of really high quality libraries, so don't feel to bad about using

    The feature flag on ureq has actually a good reason: They don't want to force you to also depend on serde_json, so all that code is optional and has to be enabled with the feature flag should you want it (in this case you do to use Response::into_json())
  • 0
    @jestdotty For the request enum: It's mostly the idea of making invalid states unrepresentable. In your code, calling send on the request enum gives back potentially any response even though each request has exactly one response "type" it returns.

    Now you have to match against that response enum to extract out the actual variant you want, resulting in a runtime panic if there is a bug. Using the type system such a bug could have been caught at compile time

    In your case just having three different concrete request and response types with three different send methods is probably the easiest way.

    For more complex scenarios you could define a trait with an associated type denoting the response type:

    pub trait Request {

    type Response;

    fn send(self) -> Self::Response;

    }
  • 1
    @jestdotty You can by making it an enum:

    enum AnyRequest {
    Balance(BalanceRequest)
    Send(SendRequest)
    }

    This is a pretty common pattern. As with the trait: Don't overcomplicate! Unless you are writing generic functions bound by that trait (<R: Request>) you don't need it. Actually you could also use it to store any request by using trait objects (e.g. a list of them would be Vec<Box<dyn Request>>) but then you can't really use them since methods returning associated types arent object safe
  • 1
    @jestdotty Yeah... inheritance is /really/ not a thing in Rust, which even to me still feels kinda wrong.

    Instead of open sets (anything that implements this) you have closed sets (anything I have a declared a variant for)

    I still am not totally happy with this, but it's how it is and it works well enough. On the plus side, you generally have way more performant code, even if it's kinda annoying

    Though to be honest, using Rust has massively improved my approach to coding. In the inheritance mindset of Java or JavaScript (I know it's prototypes but you get the point), you tend to try to "future proof" way too much, thinking about all the ways you might want to extend it in the future and designing for that.

    IMO it's so much better to code for the present, implementing what you actually need for the usecases you actually have. Rust pushes you into that direction which I really like
  • 1
    @jestdotty You'll get the hang of it after some time. Rust looks deceptively familiar but it /really/ isn't. Writing Rust is a skill all of its own

    And yeah, code architecture is probably the hardest thing in Rust. You really need to think about it before hand so that you don't paint yourself into a corner with how your ownership and lifetimes work...
Add Comment