-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Numerical Constrained Types #1621
Comments
Note that Ada also has 'mod' types for things like angles and times, which could be nice. I'm a fan of range types, but it would be nice if Rust's generic programming support would be powerful enough to support this as a library rather than building it into the language itself. You may run into annoying things like type-level floating point equality though (see: #1038 (comment)). |
On a sidenote, this has been proposed multiple times, but often rejected due to introducing typestate. I'd just like to note, that it can be done without typestates, solely with subtyping relationship defined by the subset relation. In such a case, the bounded type does not change its state (bound), since the bound is a part of the type, not the state. Thus reassigning is only possible if it is a subtype of the original declared type. One thing that would be cool with this is that you can make a vector primitive with some bound associated with its length. This makes it possible to index by some subtype of |
@bjz, mod types are a lot simpler. |
Also, I would prefer having a function like |
We already have
side-note: Rust uses even better, once we have value generics, we can make the primitive types generic over their range: type Hours = u32<0, 23>; or if we get more than just integral value generics: type Hours = u32<0..24>; Together with defaults for generics, the primitive types' generic arguments would simply default to their maximal range. This has the major advantage of
|
I didn't realize it had been discussed before. Github search didn't come up with anything of use. "Value Generics" wasn't something I would have expected as handling ranged-types. I did see the discussion on Design by Contract and thought range-types can handle some of the DbC use cases in the short term without as much effort. Thanks for the heads up on the The TryFrom looks to solves the conditional-cast part. Mod types would also be useful, especially in angles. I did realize I did not put any significant thought into arithmetic with range-types. There's so many possibilities and different use cases. With ranged types, range-overflow/underflow is very likely. Allowing an overflow without check breaks the type's contract, but is not zero-cost. One could perform arithmetic on the underlying type and return a value in that underlying type that the user must check-cast to their desired range-type. I'm thinking from a use case of scientific functions where parameters have various boundaries, but they are still operated on as numbers with some new type/unit and new set of boundaries as the result. I'll just hang back for now to see how the value generics pan out. |
More ideas in a similar style can be found here: |
I'd find these useful. In my experience it's quite common to have a type that's either an integer in some range, or one of a set of "special values". In C and C++ the special values are encoded as integer constants. In Rust there's a strong temptation to do the same thing, so that you use no more space than C/C++ would. With bounded-range types, you could write the natural type in Rust and the compiler could give you the desired representation, and you'd get run-time bounds checking so you don't accidentally convert an integer value to a special value. As noted above, in some cases this would let you eliminate bounds checks. Pascal had these in the 80s :-). |
Just a thought : A crate could supply a In fact, one could build a
so the crate could defines operation with Also, there are situations where you do not want a numeric type constrained by wrapping so much as an assertion that the type does not violate some constraints. I mentioned in the startup initialized statics thread that an attribute could specify assertions that should occur every time a type gets used, but maybe only during debugging, something like |
Constrained types could also play well with compact enums, e.g. by encoding the tag within the unused bits. Lack of support for this is a major reason why I wait with porting my C triangulator code to Rust, as I rely on packed data structures a lot (implemented using C unions and bitfields, which works really nicely and offers a healthy performance boost). |
Wonder if we could have some kind of linter extension that implements something like Liquid Haskell, with SMT solvers to check that you remain in the numeric range:
Not sure how that would interact with @ticki's const dependent type system RFC (#1657). |
I guess you should mention the case type X = f64 in ..100; // upper bound, but no lower bound aswell, as I see no reason why that shouldn't be allowed aswell. |
I'm currently working on a library for generating die rolls, for which this would be very useful indeed, especially with one-sided bounds. Dice can't, for obvious reasons, have a negative or zero number of sides. At the moment, I manually error check for this, but being able to implement such a constraint at the type level a la |
Is there any update on this? |
I also found this: https://docs.rs/bounded-integer/0.1.1/bounded_integer/#examples, but it is no longer compiling under Rust 1.32. |
Still blocked on rust-lang/rust#44580 being implemented (which it is in the process of) |
Rust now has const generics as an MVP. |
+1 for this. type Percent = u8<0..100>; This crate might be helpful for implementers : num_traits::Bounded. |
It seems Dependent type. I think it will be really useful. |
@black7375 The core capability of dependent types is to rely on runtime values. For example,
Here, if Types that restrict the available set of values and depend only on compile time values for specifying the domain, are commonly called "refinement types". |
@golddranks Yeah, you are right. Thank you for telling me good information. |
Background
Numerical types with defined constraints can be a useful way to perform some sanity checks at compile time. Ada is one of language I know which has these constraints. There may be other applications, but numbers are the main thing I find a use for them. While not a total zero-cost abstraction, the bounds checks are controlled by the user. Further bounds checks within other functions for data validation is not required as the user can know the number is within bounds.
The advantage may be in areas such as libraries or applications with formulas that have defined boundaries for parameters. A library writer may add bounds checks to each function to validate invalid data is not being sent. With numerical constraints, the library writer can define a type and know for certain the parameters are valid. The same level of certainty applies to outputs as the receiver knows the data returns is within the expected range.
I believe these types could lead to more explicit code, especially in the field of medical devices or spacecraft where the data must not be outside of expected ranges. This could be a stepping stone towards Why3-like provers or design by contract, though those could happen externally such as a MIR to Why3's IL conversion.
Looking for some comments and refinement prior to writing an RFC if this sounds sane.
Additions
The language would need two new parts: a way to specify numerical constraints and a conditional-cast capability that performs the run-time validation.
Specify numerical constraints
One way to specify constraints similar to Ada could be:
The addition would be adding the ability to specify the range of valid values. There may be some other concepts of defining constraints. I use the
in
keyword here instead ofrange
asin
is already a reserved keyword in Rust. Another option may be to allow a closure of function to be specified to handle the checks in special circumstances.Conditional-cast
The second part, conditional-casting, is required as the compiler may not know if one thing could be cast to another. I propose introducing an
as?
cast which returns something similar to anOption<T>
. If possible, having the ability for the compiler to know if a cast could be valid would be beneficial for some cases as well. Using something similar toOption<T>
allows the user to handle out of bounds situations cleanly much like doing manual bounds checking.Option<T>
itself could work, thoughNone
is a bit vague for "value could not cast."The cast system could also understand that casting to a superset is always fine, but casting to a subset requires a check. Therefore,
h as i32
compiles without a check ash
is definitely within the i32 range. If there was a second type in the range 1..24, that type also could not cast to Hours as 1..24 does not cover the entire 0..23 range.The text was updated successfully, but these errors were encountered: