`todo!()` should prevent "unused_variables" warning

The point of todo! is that you don't want to deal with the details yet. As well, in a slightly broader context it is already clear that code should be added which uses foo and bar.

fn fun(foo: Foo, bar: Bar) { // ← `foo` and `bar` are listed here
    todo!()
}
3 Likes

It does. (That's why it works with any concrete return type.)

That's the part that's missing. There's some discussion here.[1] Note that there are some thorny questions as that would mean ! implements Copy and Drop, implements other sets of mutually exclusive traits should we get those, implements Trait and !Trait given some version of negative implementations, etc.

Also it "should" implement a trait with "all" associated types for the desired result, or this still won't compile:

fn example() -> impl Iterator<Item = String> { todo!() }

  1. And elsewhere but that's the one I found off-hand. ↩︎

3 Likes

! should very much not just implement all traits. Traits have contracts that need to be upheld, and in many cases a trait doesn't require having a value of the type already, so this would just be wrong. And it would introduce a bunch of surprising panics.

Rather, example would ideally be compiled as if you had defined a brand new type and implemented Iterator<Item = String> for it.

Earlier this week I found the macro very useful when translating some pseudo-code to Rust, since I could just write expressions like if todo!("x < y") { ... } before even creating the impls. I.e. deciding how I'd like the call-site to look before starting to implement it. I'd then unwrap them one by one, fixing any issues that pop up.

Most of those issues were due to code following todo! being considered unreachable:

let x = 4;
todo!("use x");
x = 5;

This compiles, but once the macro is removed, you need to make x mutable. There were some similar issues with borrows. I think it could be more useful if it was considered diverging only when strictly necessary.

6 Likes

Did someone ever proposed this? I think this should get its own discussion. It seems very promising.

This seems like an strategy that enables type-checking any function with a diverging body and that returns an existential. Just create a new type and make your ! unify with it.

6 Likes

As far as I know ! (the "never type") very much should implement all traits. There is no instance of such a type, and yes it is very easy to derive contradictions assuming an instance of such a type, but having a simple name for it is not wrong.

The standard counter-example is Default, which very much should not be implemented for !.

5 Likes

! can't implement any traits which have associated functions (i.e. don't take self or &(mut)self). The standard reasoning why ! should implement a trait with only methods is that one can always implement such traits yourself in a canonical way: empty match on the value of ! in the receiver can return a value of whatever type the method returns.

trait Trait {
    fn foo(&self, _: Foo) -> Bar;
}

impl Trait for ! {
    fn foo(&self, _: Foo) -> Bar {
        match *self {}
    }
}

That's not necessarily the correct implementation of the trait. For example, perhaps it would be more appropriate to call some Bar-returning method on Foo. But at least it's well-defined and canonical, thus may be provided automatically.

Disregarding any other semantic requirements of a trait, it just isn't possible to provide an implementation of it for ! if the trait has associated functions. There is nothing to match on, so the only canonical (i.e. not depending on the exact trait signature and the methods of its types) implementation is a diverging one: either panic!() or loop {}. That's just no good, it silently introduces a runtime error (or worse, a hang) into the trait implementation.

Even worse if the trait has associated constants or types. There is just nothing meaningful you can put as their values.

2 Likes

To clarify: it is only impossible to automatically implement such traits for !. Manual implementations are quite often possible.

I think you do understand this distinction, but it might not be clear to future readers (I had to reread the subthread a couple times to understand the context of the claim), so I thought it is worth describing in some more detail. For example, I have a trait with an associated type, which (without irrelevant details) is:

pub trait Allocator {
    type Handle: fmt::Debug + 'static;
    fn allocate(&self) -> Option<Self::Handle>;
    // (and handles free themselves on drop)
}

This has a straightforwardly reasonable implementation for !:

impl Allocator for ! {
    type Handle = !;
    fn allocate(&self) -> Option<Self::Handle> {
        match *self {} 
    }
}

But this isn't possible for all associated types, since ! may not meet their bounds (e.g. type Handle: Default; would not be satisfiable) or might not be appropriate (if the associated type is used as an input rather than an output).

I have another trait with an associated constant which is a bool answering the question “when interacting with instances of this type, do you need to compute and provide this expensive input?" The answer to that question for ! can be false, since the input will never be read. (Or it could be true — no difference, since the situation never comes up.) Again, quite possible to do manually; impossible to do automatically.

2 Likes

Right. That's what I imply by speaking about "canonical" implementations. There may be implementations for specific traits with specific parameter types, methods and bounds on generics, but there is no way to write an impl for ! which is parametric on all of those parameters and doesn't depend on any way on their specific values. In some cases one could make choices (like a specific value of bool or u32), but no choice is better in any sense than all other choices. In other cases the choice is impossible.

Canonicity is important, because it's a kind of fixed point for whatever property one considers. Canonical things tend to compose into other canonical things, and canonical things are usually (relatively) easy to understand. Non-canonical things (values, impls etc) either don't compose, or produce gibberish in more complex cases. If we made arbitrary non-canonical choices for associated types, consts and functions, it's likely that more complex trait bounds would be impossible to satisfy anyway, because instantiating the parameters in different order would give conflicting results.

It would benefit from a way to abstract over traits, but could also be done as a small extension for object safe traits today. With a lang item with a shape like

// Ignoring the need for phantom data for ease right now
pub enum NeverForTrait<T: ?Sized> {}

One could permit impls of the form

impl<T> Iterator for NeverForTrait<dyn Iterator<Item=T>> {
  type Item = T
  ...
}

And have that be the default coercion result and such. It would have to be a lang item to make it the default result, and similar to how dyn safety works with how the compiler effectively autogenerates impls for impl Trait for dyn Trait, it could also be automated (actually probably much more easily, as we don't even need vtables!). This could be especially important for trait hierarchies, as otherwise you potentially get a lot of boilerplate drilling down it, as there is no way to specify an impl which covers all the Ts you could put in that it 'should' have.

Though the fully automatic creation of those dyn impls is possibly a misfeature, and the balancing of consistency with that vs the semvar hazard of such would be an important decision.

Unfortunately, as there is no proper way to abstract over traits, while you can define new types to set what values any associated types or consts, you can't use dyn in its current form since dyn NonObjectSafeTrait is not allowed at all. And really using dyn at all is a hack, since the argument to NeverForTrait really is supposed to be a trait, not a type.

As for the broader topic, formally speaking I agree that it should prevent unused_variables warnings. While implementation-wise any implementation of todo!() besides a panic would be at minimum confusing, one could imagine an implementation where it uses a little time machine to actually extract your code from the future and acts as a marker to remind you to close the time loop.

This shows that the return type of ! is actually too broad, since that implementation would instead return a subfunction that takes all the other arguments and passes them in, meaning none of the variables would be used. Basically having todo!() expand to gen_rest_probably_via_panic::<fn(Currently,Unused,Variable,Types)->ReturnType>()(unusedvariable1,unusedvariable2,unusedvariable3,unusedvariable4) (where gen_rest_probably_via_panic is fn<T>() -> T which errors post-monomorphization if it can't fill in the rest of your code [which it always can because it panics, and so we don't have to worry about the time machine not coming back with code]) is the abstractly correct though very silly thing to literally do, which results in the warning being suppressed.

Rather annoyingly, you can't even do todo!()(unusedvariable1,unusedvariable2,unusedvariable3,unusedvariable4) today. This would probably be fixed via the above improvement to defaulting, but makes the whole situation even more silly, and then more silly on top of that because we already have fn as 'canonical' implementations for the Fn{..} to show they are all '! safe' and show how already integrated to the compiler they are!

As for if it should in practice suppress the warning... I think also probably yes, though perhaps there should be a warning for todo!() itself (if there isn't already? I didn't see one by default on the playground and I didn't see any mention in the reference, but I didn't check the full list of warnings one can turn on).

Back to the specific case of !, similar to the dyn Trait impls for dyn-safe traits, we could also have !-safe traits and (more radically) ()-safe traits. Implementing these, as they don't involve vtables, could be done far more easily in safe code. !-safe being ones that are satisfied by ! (probably most traits, and usually with a unique implementation), and ()-safe would be a more radical option for 'defaulting', effectively a Default but for traits. Though the wisdom of having Iterator::next(Default::default()) compile without errors to what is (after inlining) None is... far more debatable.

Every other implementation of that trait for ! would be equivalent to this: it is impossible to enter the body of the function because it is impossible to instantiate self, so the compiler might as well not generate any code for the function regardless of what is in the body.

3 Likes

Actually, by the current draft opsem, &! is considered inhabited and the compiler mustn't DCE anything until it's dereferenced. For most practical purposes you're correct though.

1 Like

While being able to manually implement the code might be good, and not automatically opting into !-safe traits might be good, it still seems like an autogen option would be good.

Compare dyn-safe traits. Are we even allowed to have anything but 'use v-table' as the implementation for T for dyn T? That is definitely automatically opted into, and I don't think there is an option to manually implement alternate code either?

I'm feeling really dumb: why is Default for ! bad? Is it that generic code can't propagate the fact that the default() call must be unreachable?

Assuming the existence of any function fn foo(_: !, ...) is fine because we can never get to calling such a function, we first need to get a value of !. It's therefore fine to automatically implement traits that define such functions.

But functions and traits with ! as a type parameter can have reachable code, thus implementing Default for ! implies that we can call this code and it does something:

<! as Default>::default();

But what does it do?

Worse, <!>::deafult() specifically must produce an instance of !, which doesn't exist. For some trait Foo { fn foo(); } one can at least imagine a sensible if unlikely impl for !, while an impl of Default would be UB.

Right, but that seems to be a really general issue: if you can resolve Fn::Output to !, for example. I assume it's not specific to Default, but I'm struggling to understand why it's not already an issue, I guess?

(One approach for an auto ! trait impl would just be to panic on every call, I guess? Pretty lame, but an answer.)

Which draft? Miri and MiniRust both consider that type to be uninhabited, and the Reference also says such a type has no valid values.


But IMO the discussion of trait impls here is largely a distraction from the original question of todo! vs unused variables. FWIW I agree with the OP that these warnings should be silenced. In the current situation, warnings are largely noise while there are still todo! in the code. Useful warnings are drowned in the noise. I just ignore warnings entirely during development, which can lead to me missing lints that I actually should heed (like forgetting a ? that triggers a #[must_use] warning, or useful Clippy lints).

3 Likes

Must've been mixing up things, I suppose… I know with partially inhabited types it's more interesting.