the article says “It’s part of the standard library,” which gets the point across that it doesn’t require any external dependencies but it may be slightly misleading to those who interpret it literally - let-else a language feature, not part of the standard library, the relevant difference being that it still works in contexts that don’t have access to the standard library.
I tend to use Option::ok_or more often because it works well in long call chains. let-else is a statement, so you can’t easily insert it in the middle of my_value().do_stuff().my_field.etc(). However, Option::ok_or has the annoying issue of being slightly less efficient than let-else if you do a function call in the “or” (e.g. if you call format! to format the error message). I believe there’s a clippy lint for this, although I could be mixing it up with the lint for Option::expect (which iirc tells you to do unwrap_or_else in some cases)
I appreciate the author for writing a post explaining the “basics” of rust. I’ll include it in any training materials I give to new rust developers where I work. Too often, there’s a gap in introductory material because the vast majority of users of a programming language are not at an introductory level. e.g. in haskell, there might literally be more explanations of GADTs on the internet than there are of typeclasses
It's one lint rule which covers bunch of these _or_else functions: https://rust-lang.github.io/rust-clippy/master/#or_fun_call
Not for Python! There is way more “intro” to Python stuff out there than anything else…
[lints.clippy]
all = "deny"
unwrap_used = "deny"
expect_used = "deny"
panic = "deny"
indexing_slicing = "deny"
unhandled_errors = "deny"
unreachable = "deny"
undocumented_unsafe_blocks = "deny"
unwrap_in_result = "deny"
ok_expect = "deny"We get around it by using conditional compilation and putting the lints in our entrypoints (`main.rs` or `lib.rs`), which is done automatically for any new entrypoint in the codebase via a Make target and some awk magic.
As an example, the following forbids print and dbg statements in release builds (all output should go through logging), allows it with a warning in debug builds, and allows it unconditionally in tests:
#![cfg_attr(not(debug_assertions), deny(clippy::dbg_macro))]
#![cfg_attr(not(debug_assertions), deny(clippy::print_stdout))]
#![cfg_attr(not(debug_assertions), deny(clippy::print_stderr))]
#![cfg_attr(debug_assertions, warn(clippy::dbg_macro))]
#![cfg_attr(debug_assertions, warn(clippy::print_stdout))]
#![cfg_attr(debug_assertions, warn(clippy::print_stderr))]
#![cfg_attr(test, allow(clippy::dbg_macro))]
#![cfg_attr(test, allow(clippy::print_stdout))]
#![cfg_attr(test, allow(clippy::print_stderr))]
AFAIK there isn't currently a way to configure per-profile lints in the top-level Cargo configs. I wish there were.I try to remember to look at new restriction lints with every Rust release. For example, here's what new with 1.86.0[3]; the `return_and_then` lint looks pretty nice.
n.b. no one should enable all restrictions lints— some are mutually exclusive, some are appropriate for specialised circumstances like `#[no_std]`. But I find them helpful to keep a project away from the wild parts of Rust.
P.S. `unhandled_errors` doesn't exist[4].
[1]: https://rust-lang.github.io/rust-clippy/stable/?groups=restr... [2]: https://doc.rust-lang.org/stable/clippy/lints.html#restricti... [3]: https://rust-lang.github.io/rust-clippy/stable/?groups=restr... [4]: https://rust-lang.github.io/rust-clippy/stable/index.html#/u...
I think the only legitimate uses are for direct indexing for tile maps etc. where I do bounds checking on two axes and know that it will map correctly. to the underlying memory (but that's `clippy::indexing_slicing`, I have 0 `clippy::unwrap_used` in my codebase now).
If you begin a new project with these lints, you'll quickly train to write idiomatic Option/Result handling code by default.
Hmm, I kind of disagree. The method literally returns “OK or an error”. It converts an Option into a Result and the name reflects that.
There is something of an inconsistency though, although IMHO it’s worth it. The `Result::ok()` method returns a Some if it’s Ok, and None otherwise, which is concise and intuitive but indeed different from `Option::ok_or`.
Also, sometimes you just write software where you know the invariant is enforced so a type is never None, you can unwrap there too.
I find it interesting how a lot of people find Rust annoying because idiomatic Rust is a very strict language. You still get a ton of the benefits of Rust when writing non-idiomatic Rust. Just use the Rc<RefCell<>> and Arc<Mutex> and feel free to unwrap everything, nobody will punish you.
("This should never happen because: ..., if you see this message there's a bug.")
In general I think there is a lack of intermediate Rust material that teaches you common design patterns, idiomatic Rust, and so on.
Even I (someone who's written hundreds of thousands of fairly complex Rust code) learnt about the let-else style solution from this article =).
const user = getUser() orelse return error.NoUser;
If you only need user for a narrow scope like you would get from match, you can also use if to unwrap the optional. if (getUser()) |user| {
// use user
} else {
return error.NoUser;
} if user := get_user() is not None:
# use user
else:
# return error
Although given the happy path code can mean you don't see the error condition for ages, I much prefer this: if (user := get_user()) is None:
# return error
# use userIt actually looks really natural in python, glad they added.
fn get_user_name() -> Result(String, String) {
use user <- result.map(option.to_result(get_user(), "No user"))
user
}The error in question:
> the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result`
It literally tells you why it doesn't work, wtf do you want?
[lints.clippy]
unwrap_used = "warn"As for the article, I'm also a bit confused because I'm really not sure whether people write that sort of code at the beginning "very commonly" - match and `ok_or` to handle None by turning them into proper Errors is one of the first things you learn in Rust.
if param.is_none() {
// Handle, and continue, return an error etc
}
let value = param.as_ref().unwrap(); // or as_mut
// Use `value` as normal. let value = Some(param.as_ref()) else {
// Handle, and continue, return an error etc
}
// Use `value` as normal. let Some(value) = param.as_ref() else {
// Handle, and continue, return an error etc
}
// Use `value` as normal.That said, much like it took me a while to Grok if let syntax... this will be a struggle. See the other example in this thread where the one suggesting it reversed the syntax. That will take a bit to get right...
https://gist.github.com/tekacs/60b10000d314f9923d6b6a5af8c35...
where... in my code, I have:
some_block({ ... }).infallible()
for cases where we believe that the Result truly should never fail (for example a transaction block that passes through the inner Result value and there is no Result value in the block) and if it does then we've drastically misunderstood things.Then, there's an enum (at the bottom of the file) of different reasons that we believe that this should never fail, like:
// e.g. we're in a service that writes to disk... and we can't write to disk
some_operation.invariant(Reason::ExternalIssue)
// we're not broken, the system wasn't set up correctly, e.g. a missing env var
some_operation.invariant(Reason::DevOps)
// this lock was poisoned... there's nothing useful that we can do _here_
some_operation.invariant(Reason::Lock)
// something in this function already checked this
some_operation.invariant(Reason::ControlFlow)
// u64 overflow of something that we increment once a second... which millennium are we in?
some_operation.invariant(Reason::SuperRare)
... etc. (there are more Reason values in the gist)This is all made available on both Result and Option.