Kurtis Nusbaum
Kurtis Nusbaum

Reputation: 30825

Is there a way to create a data type that only accepts a range of values?

I have a function that takes an argument of type u16. Is there an elegant way to define a custom data type that behaves exactly like a u16 but only has values between 0 and 100?

Upvotes: 32

Views: 14487

Answers (4)

Boiethios
Boiethios

Reputation: 42759

Unfortunately, there is no such a thing inside the std crate.

However, you can do it yourself in an optimized manner with the const generics. Example:

pub struct BoundedI32<const LOW: i32, const HIGH: i32>(i32);

impl<const LOW: i32, const HIGH: i32> BoundedI32<{ LOW }, { HIGH }> {
    pub const LOW: i32 = LOW;
    pub const HIGH: i32 = HIGH;

    pub fn new(n: i32) -> Self {
        BoundedI32(n.min(Self::HIGH).max(Self::LOW))
    }

    pub fn fallible_new(n: i32) -> Result<Self, &'static str> {
        match n {
            n if n < Self::LOW => Err("Value too low"),
            n if n > Self::HIGH => Err("Value too high"),
            n => Ok(BoundedI32(n)),
        }
    }

    pub fn set(&mut self, n: i32) {
        *self = BoundedI32(n.min(Self::HIGH).max(Self::LOW))
    }
}

impl<const LOW: i32, const HIGH: i32> std::ops::Deref for BoundedI32<{ LOW }, { HIGH }> {
    type Target = i32;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

fn main() {
    let dice = BoundedI32::<1, 6>::fallible_new(0);
    assert!(dice.is_err());

    let mut dice = BoundedI32::<1, 6>::new(0);
    assert_eq!(*dice, 1);

    dice.set(123);
    assert_eq!(*dice, 6);
}

And then you can implement the maths, etc.

If you want to chose the bound at runtime, you don't need this feature, and you just need to do something like that:

pub struct BoundedI32 {
    n: i32,
    low: i32,
    high: i32,
}

You can also use a crate like bounded-integer that allows to generate a bounded integer on-the-fly with a macro.

Upvotes: 10

Ibraheem Ahmed
Ibraheem Ahmed

Reputation: 13528

With the nightly feature generic_const_exprs, it is possible to verify this at compile time:

#![feature(generic_const_exprs)]

struct If<const COND: bool>;

trait True {}
impl True for If<true> {}

const fn in_bounds(n: usize, low: usize, high: usize) -> bool {
    n > low && n < high
}

struct BoundedInteger<const LOW: usize, const HIGH: usize>(usize);

impl<const LOW: usize, const HIGH: usize> BoundedInteger<LOW, HIGH>
where
    If<{ LOW < HIGH }>: True,
{
    fn new<const N: usize>() -> Self
    where
        If<{ in_bounds(N, LOW, HIGH) }>: True,
    {
        Self(N)
    }
}

The error messages aren't the best, but it works!

fn main() {
    let a = BoundedInteger::<1, 10>::new::<5>();
    let b = BoundedInteger::<10, 1>::new::<5>(); // ERROR: doesn't satisfy `If<{ LOW < HIGH }>: True`
    let c = BoundedInteger::<2, 5>::new::<6>(); // ERROR: expected `false`, found `true`
}

Upvotes: 9

Timothy John Laird
Timothy John Laird

Reputation: 1121

Not exactly, to my knowledge. But you can use a trait to get close. Example, where tonnage is a unsigned 8 bit integer that is expected to be 20-100 and a multiple of 5:

pub trait Validator{
    fn isvalid(&self) -> bool;
}

pub struct TotalRobotTonnage{
    pub tonnage: u8,
}

impl Validator for TotalRobotTonnage{
    //is in range 20-100 and a multiple of 5
    fn isvalid(&self) -> bool{
        if self.tonnage < 20 || self.tonnage > 100 ||  self.tonnage % 5 != 0{
            false
        }else{
            true
        }
    } 
}

fn main() {
    let validtonnage = TotalRobotTonnage{tonnage: 100};
    let invalidtonnage_outofrange = TotalRobotTonnage{tonnage: 10};
    let invalidtonnage_notmultipleof5 = TotalRobotTonnage{tonnage: 21};
    println!("value {} [{}] value {} [{}] value {} [{}]", 
    validtonnage.tonnage, 
    validtonnage.isvalid(),
    invalidtonnage_outofrange.tonnage, 
    invalidtonnage_outofrange.isvalid(),
    invalidtonnage_notmultipleof5.tonnage, 
    invalidtonnage_notmultipleof5.isvalid()
);
}

Upvotes: 2

Shepmaster
Shepmaster

Reputation: 430791

As I understand it, that requires dependent types, which Rust does not have. This doesn't require dependent types (see comments) but Rust still doesn't have the support needed.

As a workaround, you could create a newtype that you verify yourself:

#[derive(Debug)]
struct Age(u16);

impl Age {
    fn new(age: u16) -> Option<Age> {
        if age <= 100 {
            Some(Age(age))
        } else {
            None
        }
    }
}

fn main() {
    let age1 = Age::new(30);
    let age2 = Age::new(500);

    println!("{:?}, {:?}", age1, age2);
    assert_eq!(
        std::mem::size_of::<Age>(),
        std::mem::size_of::<u16>()
    );
}

Of course, it doesn't behave exactly like a u16, but you don't want it to, either! For example, a u16 can go beyond 100... You'd have to reason out if it makes sense to add/subtract/multiply/divide etc your new type as well.

For maximum safeguarding, you should move your type and any associated functions into a module. This leverages Rust's visibility rules to prevent people from accidentally accessing the value inside the newtype and invalidating the constraints.

You may also want to implement TryFrom (from u16 to your type) or From (from your type to u16) to better integrate with generic code.

An important thing to note is that this newtype takes the same amount of space as a u16 - the wrapper type is effectively erased when the code is compiled. The type checker makes sure everything meshes before that point.

Upvotes: 22

Related Questions