Pre-RFC: Limited-Value Types

  • Feature name: limited-value-types
  • Start Date: 2025-04-07
  • RFC PR: none
  • RFC Issue: none

Summary

Types in Rust usually have a set of possible values, sometimes theoretically infinite but mostly not. This RFC proposes to make it possible to have a limit on how many values a type has, with compile-time constant parameters.

Motivation

The motivation for limited value types is cases where you wish to have certain values for a type and do not want to have invalid values. Eg. an error code.

// 200: Success
// 201: Create called
// 400: Bad Request
// 401: ...
// and so on
async fn my_request(data: Data, url: &str) {
      let status_code: bound(i32, STATUS_CODES) = get_status_code(data, url);
}

This could be solved with an enum:

pub enum Status {
   Success(u16),
   Err(u16)
}

But this will cause an issue if a number like 1032 is sent by mistake and also requires a lot more boilerplate for classification. Hence, a bounded number is better for easy convertibility.

Guide-level explanation

Think about a bound type as you would a normal type, except you can only use specific values. So, if you wish to use a status code and you don't want to use an enum, you can use a bound type. It's also more efficient to use. An example of how you can use this code:

Example

// If the function returns an improper value, the bound type will be incorrect.
// The bound has to exactly match the return type of the function.
let x: bound(i32, [7, 4, i32::MAX]) = receive_whatever(data);
let mut y: bound(i8, [0, 4, i8::MAX]) = 0;
y = 4;
// Whoops! I assigned an improper value to y!
// y = 8;
// error: value '8' cannot be assigned to variable of type bound(i8, [0, 4, i8::MAX])

// if the return value 3 we would get a type mismatch error
fn wow(something: i32) -> bound(i8, [0, 4, i8::MAX]) {
   4
}
// a will be an i8, not a bounded i8
let a = wow(3);
let a: bound(i8, [0, 4, i8::MAX]) = wow(3);
// bound constraints are enforced at compile time, not runtime

It's also possible to match on a bound, eg.

let status: StatusCodes = Request::get_status_code();
match status {
    200 => println!("Success!");
    // etc
}

Reference-level explanation

  • A bound type is a type with specific values for any primitive, struct, enum or slice value.
  • Said bound type will have a compile-time error if the value that is assigned to it is different from the values that have been assigned to the bound
  • All bound types implement traits directly from their original types
  • bound types can have a value be a variable, function return type or function parameters only
  • Return types of functions that are bound can be assigned to the original type, the exact same bound type, or a supertype of the bound type.
  • If an operation is conducted on two bound types, the result is the original type
  • bound types have to be annotated, they cannot use type inference
  • A bounded type does not require a constructor
  • An error will be thrown if a value that is not in the original type's purview is used in the bound type annotation.
  • A function that returns the original type may not be assigned to a bound type.
  • const values of a type or const arrays of a type may be used to create a bound type.
  • Bounds can have generics with trait objects.
  • All types of a bound can be matched.
  • Other pattern-matching can also be implemented, including if let and let else blocks.

Drawbacks

We should not do this because:

  • The type rules are too strict
  • Bounds may require a lot of repetition
  • Enums are sometimes more intuitive (mostly bounds are more intuitive)

Rationale and alternatives

Enums are possible, however require a lot of boilerplate to function properly. This provides a way for similar numerical structure to C-type enum numerals assigned, however bounded. This could be also implemented via a cenum, but I think this is enough to provide proper functionality.

Future possibilities

cenum, like this RFC discussed, is a possible way to add C-style enums to Rust to convert between other types and enums easily.

This sounds like pattern types but without the patterns. You might want to have a look at

These links are quite old. There has been more recent discussion on the topic on zulip: Public view of rust-lang | Zulip team chat

EDIT: I have just seen that the RFC has been edited in January.

8 Likes

Quite similar, but there are a few differences:

  • I think that pattern types do not include all values, only ranged ones
  • I think that pattern types only works for ints, whereas this will be available on all types

But you are right. I should add match types.

No, the idea of pattern types is that it should work with any pattern[1] (that you can write in a match).


  1. Well, any pattern that does not bind an identifier ↩︎

4 Likes