0
jestdotty
11d

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 :)
  • 0
    lol what you can't break from a for loop either

    it has to specifically be a "loop" only

    seriously wat

    so now I gotta write my own for loops?
  • 0
    @daniel-wu switch statements are known for having break statement in them. if you don't break with each switch case in most languages you'll keep falling down all the matches (can't remember if it was java and / or JavaScript, but this is also the case in bash and all those era languages)
  • 1
    @daniel-wu yeah I wrote my code fugly is what I did. now my code is running to the right of my page off my screen and you can't read it whatsoever

    early returns make readable code. people will disagree but they are wrong. do believe someone here was making fun of me for liking JavaScript because I didn't mind the cognitive overload of keeping track of what type everything is. well I don't like the cognitive overload of reading x3 as much code than I have to to get to what the code is actually doing
  • 0
    @gemsy match statements are complicated in rust. rust also has, or tries not to have any undefined behaviour to the max degree

    and you can't have null in rust. that was their whole innovation

    something cool rust does over other languages is I can assign to a variable the result of an if statement for example (and it has no ternaries which is what is typically used in other languages for that), or I can assign the result of a loop block (which I haven't seen before elsewhere), and same with match (and match has far more features than switch statements elsewhere)

    so stuff like

    let result = loop {
    let result = try_something();
    if result > 10 {
    break result
    }
    };
  • 0
    ok I think I get why this is happening

    it's violating lifetimes
  • 0
    I think the lack of early return from match is a bug though

    returning None doesn't capture any variable but in my for loop I was trying to return the variable from the thing I was looping over

    ... annnd just tried just returning a hard coded value from a for loop and it doesn't like it either. yeah I think this was their intention and they just didn't realize someone would be returning objects without lifetime violations out of these structures
  • 0
    ok nvm got it

    it's cuz you'd need an else statement if nothing in the loop matched

    though with match statements you have a catchall, so not relevant there

    I'd just say allow breaks in for loops just the return would always be an Option

    oh well

    or maybe they did some academic math on it and idk what I'm talking about
  • 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.
  • 0
    @lorentz NO EARLY RETURN

    Ok(match x {
    Thing::A => Some(12),
    Thing::B(value) => {
    let Some(value) = value else {
    //????
    return None
    };
    //do a bunch of stuff
    Some(value * 36464)
    }
    })

    this is stupid:
    match x {
    Thing::A => Ok(Some(12)),
    Thing::B(value) => {
    let Some(value) = value else {
    //????
    return Ok(None)
    };
    //do a bunch of stuff
    Ok(Some(value * 36464))
    }
    })
  • 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

    }

    }

    })
  • 1
    @12bitfloat ragh my example was again more simpler than my code

    I'm parsing a bunch of response objects and if one of the nested values can't be parsed I return None, but there's parsing beforehand and parsing after

    the last one is how I ended up doing it but I fucking hate it. no early return

    considering your level of confidence on the matter I'm gonna go with it's not possible and not in there but maybe they'll add it later
  • 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
  • 1
    @12bitfloat ye the whole rust error thing I'm not totally on board with. don't know if I like it just doing whatever until I find a way of dealing with them well

    ok if you wanna code review I could show i guess
  • 0
    @jestdotty Sure, im up for it
  • 1
  • 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
  • 1
    @12bitfloat I see

    I try to avoid using libraries as much as possible, comes from my node.js npm days and gonna be stubborn about it 😝

    request enum comment is interesting. do you have an example of that? I do think that can be improved somehow but it didn't occur to me how!

    ---

    I see the convention then is to wrap errors in more error objects. sounds so annoying

    why is turning on json feature in ureq favorable? I'm not sure why rust crates keep adding in such things as "features" when you could independently be using them, like plug and play. it seems to couple things too much to me? it ruins clear boundaries between libraries I'd think?
  • 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
    @12bitfloat yeah but you could use serde without doing it through ureq
  • 1
    @12bitfloat ok thanks for the request and response template gonna try that. I do have a list of request objects that all go in somewhere. yeah having cases that might never happen is annoying and I don't like it either
  • 1
    @12bitfloat ok the trait request way seems nice conceptually, and actually then the response objects can be the naked types without an enum. wheee

    buuut

    I can't like save all the requests I want to do to a vector of box dyn traits and save it to disk with serde

    it's always something

    so guess I'll want to rework that a different way
  • 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
    @12bitfloat ragh this is the weird sorts of things that make it all like an unintuitive straitjacket to me
  • 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
    @12bitfloat I think I was good at avoiding future-proofing

    with rust I rewrite a lot and it's slow. maybe that gets better once my brain gets used to it idk
  • 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