rust-lang / rust-clippy

A bunch of lints to catch common mistakes and improve your Rust code. Book: https://doc.rust-lang.org/clippy/
https://rust-lang.github.io/rust-clippy/
Other
11.51k stars 1.55k forks source link

Option<Option<_>> #97

Closed llogiq closed 5 years ago

llogiq commented 9 years ago

I don't know if it's used anywhere, but it does seem like misuse.

Manishearth commented 9 years ago

Suggesting an enum instead?

I dunno, the few times I've seen it used are when you have a few option-using APIs being stacked up.

llogiq commented 9 years ago

Nested options are a unary counting system. But what does the presence of the absence of a value mean? (apart from: Your program should be refactored)

Manishearth commented 9 years ago

Sure.

But there are plenty of std APIs that deal with options, and there could be legitimate use cases for using nested options in case one is composing such APIs.

Though I'm okay with linting on AST Option<Option<T>>s. My concern is mostly with expr_ty nested options.

durka commented 9 years ago

It happens (with the comment in libsyntax providing one rationale):

$ ag -Q 'Some(None)'
src/libcore/option.rs
933:                    Some(None) => {

src/librustdoc/markdown.rs
63:        markdown::PLAYGROUND_KRATE.with(|s| { *s.borrow_mut() = Some(None); });

src/libsyntax/ext/format.rs
264:    /// yet, `Some(None)` means that we've seen the argument, but no format was
269:    /// that: `Some(None) == Some(Some(x))`

src/libsyntax/parse/parser.rs
4122:            Some(None) => true,

In my code I have an enum for "sensor currently in use" and None is one of the options, so I have a Some(None) which means "I succeeded in checking the sensors, and none of them are in use". Should I be refactoring this?

llogiq commented 9 years ago

We should probably make this more clear: This lint should not catch Some(None). It should catch Option<Option<..>> types in function signatures and perhaps typedefs. To correctly match Option::Some(Option::None) would require expr_ty, as Manish noted above.

Manishearth commented 9 years ago

Though I'm okay with linting on AST Option<Option>s. My concern is mostly with expr_ty nested options.

So basically if there's a literal Option<Option<_>> ast::Ty node (not some expression of the type Option<Option<_>>, we lint). We allow it in typedefs.

This can perhaps be done cleanly in check_ty, using ast_maps parenting to check if we're in a typedef.

I'd suggest we allow nested options in typedefs actually, and disallow elsewhere.

killercup commented 6 years ago

Oh! I think we have a valid use-case for this in Diesel: Updating or skipping nullable fields (these in-progress docs for AsChangeset show how it's used). None doesn't update the field, Some(None) sets it to NULL in the database, and Some(Some(x)) sets it to x.

I know this is an edge case, but just wanted to give you a heads up as it's an edge case that we can predict some diesel users will encounter :)

cc @mikeritewho wrote a PR to add this lint

oli-obk commented 6 years ago

@killercup wouldn't an enum be better here?

enum DbUpdate<T> {
    Keep,
    Clear,
    Update(T),
}

I mean, you just had to explain to us what each of the three cases of the nested Options does ;) So that's 3 variants... which is what an enum is perfect for, right?

felix91gr commented 5 years ago

This, or part of it, was implemented in #2299. Is there still work to be done? Maybe this issue can be closed :)

flip1995 commented 5 years ago

2299 seems to implement everything discussed in this issue. Thanks for pointing this out!

schungx commented 5 years ago
enum Updater<T> {
    Keep,
    Clear,
    Update(T),
}

I'm wondering if there is a standard type of this kind in the standard library. Right now, I'm forced to use Option<Option<T>> to keep the three states: replace, no-change and clear.

It will be super easy to create one myself, obviously. However, is that a good practice to constantly re-invent the wheel? And to re-implement all the nice support methods defined for Option...

schungx commented 5 years ago

Nested options are a unary counting system. But what does the presence of the absence of a value mean? (apart from: Your program should be refactored)

It means exactly as it means: There is a value present, and it is nothing.

Option<Option<T>> is crucial to formulate tri-state values, such as:

Think of how many three-state concepts you find in a typical system and you'll know that it is a big error not to have a tri-state enum built into Rust.

Manishearth commented 5 years ago

Each tristate has different meanings though. In each of the examples you gave you really need a custom enum for it to be clear

schungx commented 5 years ago

Each tristate has different meanings though. In each of the examples you gave you really need a custom enum for it to be clear

Yes, a custom enum will work just fine. However, I'll be giving up the whole set of nice useful utility methods defined on the Option, and even on the Option<Option>> like flatten.

Or I can just copy/paste the Option source code. Neither way is very elegant.

I have thought about making a custom enum and then DeRef it to Option<Option>>... but I can't figure out how to return an &Option<Option> on the deref method form the enum.

Manishearth commented 5 years ago

The combinator method names also don't make as much sense in tristate contexts. Its not clear at all, you really should use a custom enum with well named method. Clippy is pushing you towards the right thing here.

(Deref won't work, it has to produce references and you don't have actual Options to borrow from. Furthermore, using deref for custom coercions is a bit of an antipattern)

schungx commented 5 years ago

you really should use a custom enum

I suppose so. But if everybody does that, won't every program be littered with a bunch of similar-usage types that do similar things?

A Tri-State enum comes up so often that there really should at least be a standard crate to call upon...

killercup commented 5 years ago

My personal concern mentioned above was based on the fact that a lot of code generation was involved, and I've come around over the last two years.

But if everybody does that, won't every program be littered with a bunch of similar-usage types that do similar things?

Similar, but not identical. If you wanted to have shared behavior for some part of it, you might want to have a derive macro that allows you to flag one variant as "empty" or one as "full" and get some methods like unwrap for free.

But I would not shy away from writing new "simple" types like these. They tend to make the code so much more readable that the small amount of needed boilerplate is – IMHO – quite acceptable. It is kinda the same as using custom enums instead of bool all over the place, just one step more abstract :)

Manishearth commented 5 years ago

Again, they aren't the same enum, the implications of each state are different in each case, unifying them leads to a loss of clarity. I've come across this situation a bunch and it's been exceedingly rare where some generic tristate would have led to more clarity.

Put a different way: we use C-like enums a ton, "why don't we just use integers instead?". Just because things are represented the same doesn't mean they're logically the same