Do you use the available types to express intent/semantics?

Say you're taking in a queue size argument, where a size of 0 not only does not make sense, but it would make the function/library not work properly. Do you take in a usize (and return error/panic on 0) or a NonZeroUsize?

Out in the wild, I very rarely see people use things like NonZeroUsize (other than put it in Option).

Personally I tend not to use it because it always felt like it would be to much bureaucracy for the user of the API. But lately I've been toying around with special-purpose wrappers around types to more clearly state intent, and I've discovered that I hate it far less than I had originally thought.

On a scale between 0 and pi, how angry do users get if they have to pass std::num::NonZeroUsize::new(16) instead of just 16?

I tend to use NonZeroUsize, but the two doesn't necessarily exclude each other. I can implement a "narrow" version with NonZeroUsize, then create a "wide" version which wraps the "narrow" one with runtime checks.

that's a good point. I think the lack of ergonomics to create non-zero literal values is one of the biggest reasons why people avoids NonZeroXXX types. but it's possible to relief the annoyance by API designs. for example,

fn foo_narrow(x: NonZeroI32) {
    todo!()
}
fn foo(x: impl TryInto<NonZeroI32>) {
    foo_narrow(x.try_into().unwrap())
}

fn main() {
    foo(42);
    foo_narrow(NonZeroI32::new(42).unwrap());
}
1 Like

Thank you -- this is a really nice "best of both worlds" solution that hadn't occurred to me.