The std::num::NonZero<T> type allows non-zero integer semantics to be clearly expressed. Yet this
type is only seamlessly usable if all APIs with non-zero semantics use this type, due to required
to/from conversion at any API boundary that differs in its use of NonZero.
The burden of these conversions is especially heavy in tests and examples. This RFC proposes new
coercions to facilitate implicit conversion to NonZero from integer literals, simplifying usage
in tests and examples, where succinctness and readability are paramount.
Motivation
Using NonZero to express non-zero semantics is valuable because it leads to clarity of APIs,
removes the need for some runtime zero-checks and creates potential for niche optimizations.
In typical logic code, NonZero<T> values are validated early in the process, such as when parsing
user input. We start with a value of integer type T which may or may not be zero, and we parse
this into a NonZero<T> - if this succeeds, we know it is not zero and can skip zero-checks in any
further calls, as long as the API surface uses NonZero<T>.
However, there is one area where NonZero has particularly poor ergonomics: tests and examples!
In test code, numeric constants are common. By switching a function parameter from u32 to
NonZero<u32> we add needless complexity to the test code.
Without NonZero:
With NonZero:
use NonZero;
Having to manually construct the NonZero wrapper in test code can become very noisy and is
especially problematic in example code and doctests, where crate authors want to put their best
foot forward, to show off how easy it is to use the crate. This is because the nature of NonZero
creates a difference between the two categories of usage:
- In real use, the
NonZerois typically constructed when parsing the input, not when calling some API that expresses its parameters viaNonZero. - In test and example use, there often is no parsing stage, so hardcoded test input must be manually wrapped at call sites.
This means that tests and examples are much more noisy than real-world usage for an API that uses
NonZero, giving a false impression of API complexity and discouraging API authors from using
NonZero despite its advantages.
In test and example code this commonly occurs with integer literals. The values are known and can be validated at compile time to be non-zero and within expected bounds.
This RFC proposes that we omit the ceremony with literals, allowing implicit coercion of non-zero
integer literals to NonZero<T>, thereby encouraging Rust crate authors to
make more extensive use of NonZero, which they may today choose to avoid due to the extra cruft
it adds to tests and examples.
With this RFC implemented, this would be valid code:
use NonZero;
Guide-level explanation
The std::num::NonZero<T> type can be used to express that an integer of type T has a non-zero
value. This helps clearly express the intent of the API and encourages "thinking with types",
whereby function calls are maximally validated at compile time, reaching a state where if the code
compiles, it has a high probability of being correct.
In typical usage, you convert from a T such as u32 into a NonZero<T> when parsing the input
and thereafter keep the value in the NonZero wrapper:
use ;
You can pass this value as-is to functions that take NonZero<u32>:
When writing test or example code and using hardcoded constants, you can omit the conversion into
NonZero<T> - it is done implicitly at compile time. This only works with integer literals
like 1234.
This is similar to how in the statement let foo: u8 = 123;
the literal 123 is inferred to be u8.
Being able to skip the NonZero::new helps avoid unnecessary complexity in tests, encouraging high
test coverage and making test code easier to read. It also helps focus examples and doctests on the
usage of the NonZero capable API that you are publishing, without unnecessary noise from parsing
of constant values that would typically happen elsewhere in real code.
Reference-level explanation
Untyped integer literals (e.g., 123, 0xFF but not 123_u32) are implicitly coerced
to std::num::NonZero<T> (T being u8, u32, etc.) if all of the following are true:
- The value of the literal is not 0.
- The value of the literal fits within
T’s range (e.g.,300fails forNonZero<u8>). - The target type is explicitly
NonZero<T>or inferred as such. - The type
Tis unambiguously resolved from the targetNonZero<T>type.
The coercion happens at compile time, with the emitted code being the equivalent of
const { NonZero::new(literal).unwrap() } for valid cases, with no runtime checks.
The coercion is allowed when the source is an integer literal (123) or an integer literal
negation expression (-123). The coercion does not apply to other expressions besides unary
negation, even if the expressions combine literals (e.g. 123 + 456 does not qualify).
foo; // OK
foo; // OK
foo; // OK
foo; // Error - value cannot be zero.
foo; // Error - out of bounds of i8.
foo; // Error - only untyped literals accepted.
foo; // Error - literal has non-matching type.
foo; // Error - coercion not applied for expressions.
const MAGIC_VALUE: = 123; // OK - coercion logic is same as when calling a fn.
let i = 123;
foo; // Error - the coercion only applies to literals and `i` is not a literal.
Drawbacks
Any implicit behavior in the language risks becoming "magic" that is hard to understand and reason
about. For this reason such behaviors are very uncommon in Rust. The greatest drawback is that we
may open a box that also contains some demons in addition to this simple enhancement - what other
implicit behaviors might get proposed using this as an example? The fact that this RFC is scoped
to constants and not variables is the mitigating factor here. After all in let foo: u8 = 123 the
literals becomes a u8 and is not too different in nature from let foo: NonZero<u8> = 123, though
the mechanics are of course different.
Rationale and alternatives
An alternative with less magic would be to require an explicit "nonzero" suffix for constant literals to activate this coercion:
foo; // Error
foo; // OK
foo; // OK
This would still eliminate most of the cruft from tests and examples while still requiring an explicit action by the user to invoke the coercion.
Given that the potential for "confusion by too much magic" seems minimal as the concept of "non-zero" is trivial to understand and the proposal does not involve any size-coercion, the value of avoiding fully implicit coercion here seems low - there is not an obvious benefit from only taking half a step toward an improvement.
If we consider a library approach rather than a language approach, one viable alternative is to create a macro to easily construct non-zero values and validate that they really are non-zero. For example:
use nonzero;
foo; // Error
foo; // OK
This macro will attempt to construct the value in a const context and emit a meaningful error if
that fails. This may offer a satisfactory user experience if such a macro were included in std,
though still comes at a cost of an unusual level of cruft for merely passing integer constants to
code, and remains something the user of the code has to think and know about.
Given that passing integer constants to code is an everyday task for a programming language, that
the NonZero wrapper type exists already in std and that there are so far no known corner cases
that could not be easily verified correct at compile time, having it "just work" seems to offer the
best tradeoff of factors to encourage wider usage of non-zero semantics, yielding more correct and
potentially more efficient code in APIs where NonZero is applicable.
That said, all these alternatives are better than what we have today - manually having to construct
an instance of NonZero<T>.
Previous discussions
have suggested that other ergonomic challenges exist with NonZero (e.g. because it lacks many of
the methods that you would expect to find on primitive integer types it wraps) and that improving
its ergonomics across the board would likely be challenging, if not due to the mechanics then at
least due to the scope of functionality associated with numeric types. Nevertheless, this does not
appear to be a meaningful argument against making NonZero ergonomics better for the scenarios
where it is useful to Rust users today, even if those cases are more limited than raw numerics.
In practice, NonZero types do appear to be gaining new members, so
there does not appear to be a consensus against improving them (e.g. 1.84 stabilized
NonZero::isqrt)
Prior art
Exploration of other languages suggests that while refinement types like NonZero are common, they
generally require explicit conversion as they are not specific to integers as a general language
feature. In contrast, this RFC deals with integer refinement in particular, as the NonZero types
are focused specifically on this mode of refinement.
Similar functionality appears to be present in Ada if we use its subtyping feature to
define a NonZero_Int type:
with Ada.Text_IO; use Ada.Text_IO;
with Ada.Integer_Text_IO; use Ada.Integer_Text_IO;
procedure Item_Fits_Test is
subtype NonZero_Int is Integer range 1 .. Integer'Last;
function Item_Fits_Exactly_In_Packaging(Height : NonZero_Int) return Boolean is
begin
-- No need to check for zero; the subtype guarantees it's non-zero
return 1000 mod Height = 0;
end Item_Fits_Exactly_In_Packaging;
procedure Test_Item_Fits_Exactly_In_Packaging_If_Divides_1000 is
Result : Boolean;
begin
Result := Item_Fits_Exactly_In_Packaging(1);
pragma Assert(Result, "1 should divide 1000 evenly");
Put_Line("Test 1 passed");
Result := Item_Fits_Exactly_In_Packaging(3);
pragma Assert(not Result, "3 should not divide 1000 evenly");
Put_Line("Test 3 passed");
Result := Item_Fits_Exactly_In_Packaging(25);
pragma Assert(Result, "25 should divide 1000 evenly");
Put_Line("Test 25 passed");
Result := Item_Fits_Exactly_In_Packaging(999);
pragma Assert(not Result, "999 should not divide 1000 evenly");
Put_Line("Test 999 passed");
Result := Item_Fits_Exactly_In_Packaging(1000);
pragma Assert(Result, "1000 should divide 1000 evenly");
Put_Line("Test 1000 passed");
-- This would cause a compile-time error if uncommented:
-- Result := Item_Fits_Exactly_In_Packaging(0);
-- Error: constraint error, 0 not in range 1 .. Integer'Last
end Test_Item_Fits_Exactly_In_Packaging_If_Divides_1000;
begin
Test_Item_Fits_Exactly_In_Packaging_If_Divides_1000;
Put_Line("All tests completed successfully!");
end Item_Fits_Test;
Pattern types rust#123646 are a generalized form
of the concept underpinning NonZero and may offer a generalized solution to the problems described
in this RFC, if they are adopted into Rust.
Unresolved questions
The alternatives presented above appear inferior but only slightly so - we should carefully consider which strategy to apply here, especially if there appear corner cases not yet explored, where the different approaches may show off their respective strengths.
Future possibilities
If we accept that NonZero<T> deserves to be implicitly coerced from non-zero values of T, the
experience from implementing and stabilizing this may offer valuable insights for how deeply and
how explicitly/implicitly to integrate other bounded/restricted types such as
Bounded<T, Min, Max> with custom minimum and maximum values or other types of refinement.
This is out of scope of this RFC. Related discussions: