Simple dependent types in const contexts?

This is a very poorly flushed out idea, but could you see a future with dependent types in const contexts? I'm specifically thinking about cases like NonZeroU8::new() which is implemented very simply like this:

if n != 0 {
  Some(Self(n))
} else {
  None
}

Using the NonZero types is super painful right now: you can either use some unsafe (unsafe {NonZeroU8::new_unchecked(1) }) or unwrap (NonZeroU8::new(1).unwrap()) and trust the optimizer to remove all the bloat. Both solutions suck. Real world example I've written before:

thread::available_parallelism().unwrap_or(unsafe { NonZeroUsize::new_unchecked(1) })

I actually think I should have just used unwrap since I trust the optimizer to figure it out, but that's deeply dissatisfactory from a "let me check the places my code could crash" standpoint. (And unsafe is dissatisfactory because it's unsafe.)

Proposal: in const contexts, if would be cool if we could just get the NonZero type. So the signature would be something like fn new(n: u8) -> {None,Self} and your code would fail to compile if you expected the type to be NonZero but it was actually None (and vice versa, but I don't know why you'd want that).

I've read past threads on dependent types, but they seem to focus on the proof aspects. This idea would hopefully be narrower with the goal of just solving the above problems. Then again, that probably makes this feature way too narrow for the complexity it would introduce.

2 Likes

Before addressing API issues and "is this something that is a good idea", this raises a fundamental problem in computer science. Even in const contexts, this immediately brings up the halting problem. Type checking needs to occur prior to constant evaluation for the most part. And a constant expression can sufficient constructs to produce a turing machine, so we'd be adding yet-another-way that rust type checking is impossible in the general case.

I think the answer is to use unwrap, but to force it at compile-time:

thread::available_parallelism().unwrap_or(const { NonZeroUsize::new_unchecked(1).unwrap() })

using inline_const - The Rust Unstable Book.

9 Likes

Dang lol. Scott's answer I think gets 90% of the way there which is good enough.

1 Like

This is awesome! The unwrap is still a little ugly, but I now I can force the compiler to prove the invariants:

#![feature(const_option)]
#![feature(inline_const)]

use std::num::NonZeroUsize;

fn main() {
    let _z = const { NonZeroUsize::new(0).unwrap() }; // COMPILE-ERROR, woohoo!
}

On stable Rust, you can use a macro:

use core::num::NonZeroUsize;

macro_rules! const_nonzero {
    ($n:literal) => {{
        const RESULT: NonZeroUsize = match NonZeroUsize::new($n) {
            Some(result) => result,
            None => panic!("tried to make a zero NonZero"),
        };

        RESULT
    }};
}

fn main() {
    let a: NonZeroUsize = const_nonzero!(3);
    dbg!(a);
    // dbg!(const_nonzero!(0)); // doesn't compile
}
1 Like

What I really want is for

thread::available_parallelism().unwrap_or(1)

to just work, and for

thread::available_parallelism().unwrap_or(0)

to be a compilation error.

After all, we can already have 200 infer to a bunch of different possible types, and give errors for i8. So why not a few more possible types for the literals?

4 Likes

^^ Eventually I hope to get around to nonzero literals. It would need a little bit of hacking on the compiler, but I'm positive it's doable. My previous attempt only worked in some situations.

2 Likes

Personally, I would use:

std::thread::available_parallelism().map_or(1, From::from)

Or, better, in a context in which I can use ?:

std::thread::available_parallelism()?.into()

I don't typically want to keep using a NonZero type any longer than absolutely necessary.

I feel like that's a reflection that it's too hard to use the nonzero types right now, which might be a solvable problem.

I'd love for us to get the mythical const geneneric versions of arbitrary integer types where they've been made nice to use so one doesn't need to fall back to the-closest-power-of-two types, but also offer all the other combinations that people want (like the "unsigned but not max" and such).

12 Likes

I would expect this feature to come as a facet of a more general "pattern types" feature that also includes enum variant types:

That's not typically the reason. My most common reason is that I know I want to use normal integer types everywhere, and I'd rather convert once when getting the value that uses this type rather than a dozen times when I pass it to something that uses an integer type. My other common reason is that I want to do math on something and don't expect to be able to satisfy the dependent-typing to keep it in NonZero form.

How is keeping a number in NonZero form meaningfully different from preventing an overflow on u32/i32?