So, you want an integer, but one value sacrificed to denote not-a-number? Is this what "niche" means?
So that with these, you can have Option[integer8, None], an option type that either contains a -127 to +127 8bit integer or contains the empty value, and still consumes only 8 bits of memory? Or are there other uses for this?
And Rust already has this, and with the memory optimization, but for some reason only the version where they sacrificed the value 0.
You're correct that Rust provides NonZeroI8, which in fact I called out in my text. If you look at the source, you'll see that the Rust standard library is allowed to make this type but you can't in stable Rust,
[rustc_layout_scalar_valid_range_start(1)] means, "Hey, Rust compiler, I promise it's fine to just assume I never have this value: 0". You absolutely could write BalancedI8.... if you're the Rust standard library. Unfortunately you aren't allowed to do that in your own stable Rust software because it needs these compiler internals.
There were discussions about stabilising this idea, and they were knocked back because if you do that you're basically stuck with it forever.
One day in the future NonZeroI8 could "just" be a Pattern Type, like the ones you can make at home, but today there are no Pattern Types and NonZeroI8 needs compiler magic.
There are other Rust core types which you could make yourself, IPv4Addr for example is just a 32-bit value, you could make your own, but yours wouldn't be on everybody's Rust system. Option<&T> is just a sum type over &T and None, you genuinely could make your own, call it Maybe<&T> and it would work exactly the same - same optimisations even, although not all the compiler diagnostic messages would be as good if you use it wrong I think. But today NonZeroI8 isn't like them, it actually needs a sprinkle of compiler magic.
The value of encoding this case in the type system seems minimal to me though.
Edit: Thanks for the explanations. From what I see, pattern types are subtypes of a given type, such that the subtype predicate can be specified as a pattern match. This way, pattern types are subtypes that can be checked at compile time (at least, if the used pattern can be checked at compile time, I don't know if all Rust patterns are compile-time checked).
subtype Rainbow is Color range Red .. Blue;
subtype Small_Int is Integer range -10 .. 10;
[0] https://www.adaic.org/resources/add_content/standards/05rm/h...To be honest, it wasn't particularly useful, because it requires runtime checks which panic if they fail.
What is their practical use in the modern age?
This lets us have all the performance advantages of using magic sentinel values occupying those bit pattern, but with the same ergonomics as for an ordinary sum type.
For example in C a Unix file descriptor is just an integer. 0, 100, 1000 - all perfectly reasonable file descriptors. But, -1 is not a valid file descriptor, so Rust's OwnedFd is internally just an ordinary C-style integer, except, it's never -1 as a result Rust's Option<OwnedFd> is the same size as the C integer, you'll get the same machine code as the C integer, but in C you need to remember to check it's not -1 before using it, in Rust you won't make that mistake because that's not Some(fd) that's None.
Rust does this with its references, Option<&T> is the same size as &T, depending on what exactly T is that's probably "really" a machine address in a CPU register, and so None is the same CPU register with an all-zeroes bit representation.
My favourite non-standard library use of this feature is CompactString. CompactString is the SSO (Small String Optimisation) made famous in C++ but applied to Rust's strings. Rust's native String type is as simple as possible, thus no SSO, it's actually internally Vec<u8> plus rules to ensure it is always UTF-8 encoded text. SSO in C++ standard libraries means that "Dog" or "Cheese" are stored inline in the type itself, no need for a heap allocation. CompactString takes that to an extreme. While a typical C++ std::string might allow you to store "ycombinator.com" inside the 32 byte data structure, CompactString fits "https://ycombinator.com/" in its just 24 bytes!
It does this by being able to distinguish whether that last byte is a valid final UTF-8 code unit, if it is then this is a 24 byte string, but if it's not then it signals how long the rest of the string is and how the other 23 bytes should be interpreted.
https://devblogs.microsoft.com/oldnewthing/20240510-00/?p=10...
The presentation right before the Rust one[1] actually did a bit of a dive into this.
I recently created a UUID library in Ada, and I'm able to validate an input string on the datatype level without ever having to worry about doing so in the function itself:
subtype UUID_String is String (1 .. 36)
with Dynamic_Predicate =>
(for all I in UUID_String'First .. UUID_String'Last =>
(case I is
when 9 | 14 | 19 | 24 => (UUID_String (I) = '-'),
when others =>
(UUID_String (I) in '0' .. '9' | 'A' .. 'F' | 'a' .. 'f')));
Now my function simply needs to be: function From_String (From : UUID_String) return UUID
with Pre => From in UUID_String;
And I can code the function with the confidence that it won't be processing a malformed string.[1] I don't think the video is ready yet, but you can view the slides here: https://fosdem.org/2025/events/attachments/fosdem-2025-4879-...
Some type safety conditions can naturally only be checked at run-time.
And actually, checks are done at compile time if the compiler is able to see what is being used on the spot.
In Rust NonZeroI8::new(n).unwrap() is either a NonZeroI8 or, if n was zero, it panics because we asked to unwrap the Option and that's None.
What's inside NonZeroI8::new ? Literally a cast. When compiled this is nothing at all, no machine code is generated. It is relying on the fact that Option<NonZeroI8> has bit pattern 00000000 for None, and that's also the bit pattern for the integer zero.
So if we ask at compile time, NonZeroI8::new(FOO).unwrap() for some constant FOO, the compiler will, at compile time, transform the FOO bit pattern, if it's all zeroes this code is just a runtime panic, and if it's on the clear through line (e.g. the only code in the main function) by default the compiler says well that's not going to work, here's a compiler error [if you actually want a program which just panics when run you can ask for that with a compiler setting, good luck to you].
If FOO isn't zero then, still at compile time, now we've got a NonZeroI8 with value FOO
Checking at runtime is 100% still incredibly useful. That's how you enforce critical program invariants to avoid security vulnerabilities or prevent invalid states that could ruin the program's data.
* Using the same range for arrays indexes, "for" loops parameters which index those arrays. In those cases a good compiler removes the useless run-time checks!
Ada range types can have bounds that are known only at run-time. It was not possible with Pascal.
To see subtypes in action: another FOSDEM presentation:
https://fosdem.org/2025/schedule/event/fosdem-2025-5148-adve...
Pattern types or liquid types or dependent types are a way to express a subset of a type. E.g.
type NonNull = usize is 1..
type PokerNonFaceCard = u8 is 2..11My first job was mostly SPARK Ada, subtypes were so useful, both in terms of making contracts clearer for the human and for the various analysis tools.
Rust is high on my list of languages to learn when I can make some time, having something like this available will be great.
https://docs.rs/nonmax/latest/nonmax/
If you're really attached to it being min you'd have to copy that library.
Edit to add: we actually use these in one of my main Rust codes; they're useful, but I'm not sure they're so useful I'd want them built into the language.
† Just in case you've never looked inside those types, they have a constant value and when you want a NonFoo you just take a NonZero and you XOR it with that constant, in other respects they work the same way as any other wrapper type. This is one reason the NonZero types exist, they make the XOR trick very easy to do.
Actually instead of just removing the most negative number i8::MIN you can appropriate its bit patter to denote "missing value" (or "null" if you wish) so that your new data type is symmetric and nicely "nullable".
In case you didn't know yes, that's how Rust's niches work and so that's what we're talking about in this sub-thread.
I want Pattern Types for an entirely selfish reason, I want to write BalancedI8 and similar types -- as user defined types, but in stable Rust and have them Just Work™.
BalancedI8 is similar to i8 (an 8-bit signed integer) and to NonZeroI8 (the same type but, with a niche where zero should be), instead of removing zero, which we often have a use for, lets remove the annoying most negative value i8::MIN
Why "BalancedI8"? Because now that we've removed this most negative number (to provide a niche) the remaining value is symmetrical, it goes from -127 to +127
Now, I profess that I want these types but I clearly don't want them enough to do very much about it, so that's not great.