News for hackers. Stuff that matters.
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by tialaramex on Monday February 10, 2025 @04:34AM (#42998552)
Oh! It's about Pattern Types!

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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by sampo on Monday February 10, 2025 @07:57AM (#42999686)
> I want to write BalancedI8

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.

https://doc.rust-lang.org/std/num/type.NonZeroI8.html

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by Joker_vD on Monday February 10, 2025 @08:47AM (#43000242)
The main use is not having to deal with INT_MIN, the two chief problems of which is that you a) you can't sensibly abs() it, b) most people tend to forget about point a). This problem in my experiences most commonly arises in naive implementations of itoa/etc. which tend to assume that they can just call abs() and concentrate on the positive half-range signed integers. Nope, you can't do this, not all negative integers have a positive counterpart.
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by tialaramex on Monday February 10, 2025 @11:08AM (#43001843)
Yes, I want to be able to make such types myself.

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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by edflsafoiewq on Monday February 10, 2025 @11:22AM (#43002020)
They're sort of used in graphics APIs. When a value in [-1.0, 1.0] is quantized to an i8, the result is a BalancedI8: -1.0 maps to -127, 1.0 to 127, and -128 is unused; if -128 is used in the dequantizer, it is treated the same as -127. This replaced the old scheme where -1.0 mapped to -128 in which 0.0 was unrepresentable.

The value of encoding this case in the type system seems minimal to me though.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by ziml77 on Monday February 10, 2025 @06:47PM (#43006836)
The reason to encode it in the type system is it allows for optimization. Option<NonZeroI8> uses a single byte because it knows 0 is available to represent the None state. Option<BalancedI8> could do the same thing with -128 representing None.
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by auggierose on Monday February 10, 2025 @06:21AM (#42999107)
What are pattern types? Cannot watch the video, too slow.

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).

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by amelius on Monday February 10, 2025 @06:32AM (#42999170)
The term "pattern type" is more or less invented by Rust. See this old comment, and the first reply in particular:

https://news.ycombinator.com/item?id=39570633

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by kreco on Monday February 10, 2025 @06:39AM (#42999203)
And from what I get it's basically the same idea as Ada's "subtyping"[0].

    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...
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by hackyhacky on Monday February 10, 2025 @06:46AM (#42999233)
We could do that in Turbo Pascal, 35 years ago.

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?

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by tialaramex on Monday February 10, 2025 @07:20AM (#42999436)
In Rust the bit patterns which are not occupied by a value are free for some other use, Rust calls this a "niche" and uses it extensively in its sum types.

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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by kccqzy on Monday February 10, 2025 @09:49AM (#43000928)
Minor correction about C++ SSO: different C++ implementations have different small string capacities. In clang and libc++, you can store 22 bytes of short string when the data structure itself is 24 bytes. This is more than what MSVC or gcc can do.

https://devblogs.microsoft.com/oldnewthing/20240510-00/?p=10...

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by ajdude on Monday February 10, 2025 @07:03AM (#42999326)
With Ada/SPARK (and hopefully Rust) this can be checked at compile time to formally prove code. You can also catch the runtime exceptions and handle them accordingly if you're not using SPARK.

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-...

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by pjmlp on Monday February 10, 2025 @08:19AM (#42999881)
Just like bounds checking in Rust.

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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by tialaramex on Monday February 10, 2025 @11:34AM (#43002164)
Also, sometimes you can arrange that the "check" is just the natural way to express what you meant in the language, so whether it's a runtime check or not, the ergonomics are improved enormously, it feels natural.

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

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by naasking on Monday February 10, 2025 @10:16AM (#43001266)
> To be honest, it wasn't particularly useful, because it requires runtime checks which panic if they fail.

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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by etrez on Monday February 10, 2025 @02:24PM (#43004145)
* Splitting large enumerated types into smaller ones

* 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...

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by Tomte on Monday February 10, 2025 @06:55AM (#42999276)
According to the talk it‘s more about Dynamic_Predicates, i.e. arbitrary conditions on a type. He‘s showing an Ada type definition for prime numbers, not just simple subranges.
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by Ygg2 on Monday February 10, 2025 @06:40AM (#42999209)
They're called pattern types because the intention is to allow them to match a Rust pattern expression (more or less, no idea how conditionals would work).
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by Lucretia9 on Monday February 10, 2025 @09:54AM (#43000985)
Pattern matching is in all functional languages, afaik.
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by mbStavola on Monday February 10, 2025 @11:52AM (#43002348)
As an update to my previous comment in the linked thread, the aforementioned minimal version of pattern types have been merged and generic support is currently being implemented[0]. Slowly but surely, we're getting there!

[0]: https://github.com/rust-lang/rust/issues/136574

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by the8472 on Monday February 10, 2025 @06:37AM (#42999193)
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by Ygg2 on Monday February 10, 2025 @06:38AM (#42999197)
There is a pdf of presentation in the link...

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..11
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by Yoric on Monday February 10, 2025 @05:02AM (#42998723)
Having pattern types would be great!
Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by noneeeed on Monday February 10, 2025 @09:12AM (#43000504)
Nice!

My 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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by eslaught on Monday February 10, 2025 @02:20PM (#43004099)
Maybe not fully ergonomic yet, but this exists today (at least for max):

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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by tialaramex on Monday February 10, 2025 @02:37PM (#43004292)
Yeah, the XOR trick†. It's very affordable on modern hardware (your CPU can cheerfully XOR two registers while doing other things, barely measurable) but it's more the principle of the thing. Maybe I should just put up with the XOR trick and stop thinking about Pattern Types but I don't think I'll be able to.

† 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.

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by lr1970 on Tuesday February 11, 2025 @06:36AM (#43011654)
> 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

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".

Re:FOSDEM 2025: The state of Rust trying to catch up with Ad... by tialaramex on Tuesday February 11, 2025 @01:46PM (#43016599)
I can't tell whether you don't know what a Rust niche is, or you do but you thought we didn't

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.