You can create NonZero numeric types and check at compile time that they are in fact, nonzero like so:
const { NonZeroU8::new(4).unwrap() }
But, it requires using a const block and an unwrap. It would be great to be able to just have a literal like 4 coerce into a NonZeroU8 similarly to how a literal such as 4 can coerce into a u8 or a u32
I'm proposing some kind of mechanism to allow users to define automatic coercion from an untyped numeric literal (such as 4, -11, but not 4u8) into an arbitrary type. For example via some const Trait.
Then, types such as NonZeroU8, NonZeroU32 could implement this Trait. You could then freely use the untyped numeric literals in places where NonZeroU8 and etc are expected.
An example:
use std::num::{NonZeroU8, NonZeroU16};
fn takes_nonzero_u8(n: std::num:::NonZeroU8) {}
fn main() {
// 4 can coerce into a NonZeroU8, which is checked for correctness at compile time
let a: NonZeroU8 = 4;
takes_nonzero_u8(44);
// this would not compile:
let b: NonZeroU8 = -4;
// similar to:
// let b = const { NonZeroU8::new(-4).unwrap() }
}
Example definition of the u8::Coerce trait:
#[const_trait]
trait Coerce {
fn coerce(n: u8) -> Self;
}
Example Implementation and usage of u8::Coerce for a struct NonZeroU16:
struct NonZeroU16 {
inner: u16,
}
/// Any numeric type that can coerce to
/// a u8 will also be allowed to coerce to a NonZeroU16
impl const u8::Coerce for NonZeroU16 {
fn coerce(n: u8) -> Self {
if n == 0 {
panic!("Cannot be zero");
}
Self { inner: n as u16 }
}
}
fn main() {
// compiles
let a: NonZeroU16 = 4;
// compile error: expected NonZeroU16, found u8
let a: NonZeroU16 = 4u8;
// compile error: Cannot be zero
let c: NonZeroU16 = 0;
}
This trait (u8::Coerce) would only work on untyped numeric literals that can coerce to a u8. All of the below examples will fail to compile:
fn main() {
let a: u16 = 4;
// compile error: expected NonZeroU16, found u16
let b: NonZeroU16 = a;
// compile error: expected NonZeroU16, found u16
let c: NonZeroU16 = 4u16;
// compile error: 1000 cannot coerce to u8
let d: NonZeroU16 = 1000;
// compile error: expected NonZeroU16, found u16
let e: NonZeroU16 = 4_u8 as u16;
}
However, if we also implement u16::Coerce then those examples will compile:
impl const u16::Coerce for NonZeroU8 {
fn coerce(n: u16) -> Self {
if n == 0 {
panic!("Cannot be zero");
}
Self { inner: n }
}
}
fn main() {
// compiles now
let d: NonZeroU16 = 1000;
// literals that can coerce to *either* u8 or u16 can
// coerce to a NonZeroU16
}
Note: Since we implemented both u16::Coerce and u8::Coerce for NonZeroU16, we could remove the u8::Coerce implementation as untyped literals that can coerce into a u8 can also coerce into a u16.
Summary
We would add the following traits:
std::u8::Coercestd::u16::Coerce- ...
std::i8::Coercestd::i16::Coerce- ...
std::f16::Coercestd::f32::Coerce- ...
std::usize::Coercestd::isize::Coerce
When some type T implements $num_type::Coerce (where $num_type is any of the numeric types u8, i8, f32 ... ), any untyped numeric literal (Such as 4, -12.4, but not 100_u8) that can coerce to $num_type will be able to be implicitly converted to T by using $num_type::Coerce::coerce
Advantages
- Improves ergonomics when creating number literals that must adhere to specific invariants checked at compile time
- Allows implementing this trait for custom structs, such as a
NonZeroU32that must never be zero orEvenU32that must always store an even number. - Does not rely on new syntax (such as adding new numeric type suffixes)
Disadvantages
- Verbose implementation: Adds a new trait to the standard library for each numeric type
- Adds more implicit coercions to the language
- Relies on
#![feature(const_trait_impl)] - Cannot be implemented in Rust, requires changes to the compiler itself
Alternatives
- Specifically for
NonZero*, we could add custom suffixes such as16_nzu8forstd::num::NonZeroU8. This means we will lose the flexibility of a generic implementation, as well as adding extra syntax - Do nothing, and when we want to create a type like
NonZeroU8useconst { NonZeroU8::new(4).unwrap() }