How to create a typesafe range-limited numeric type?
Asked Answered
F

1

6

In Rust, I have need of a numeric type with the property of having a domain symmetric around 0. If a number n is a valid value, then the number -n must also be valid. How would I ensure type-safety during initialization and arithmetic? How would it be best to implement modular and saturation arithmetic on the type?


The simplest example of the problem is:

type MyNumber = i8; // Bound to domain (-100, 100)

fn main() {
    let a = MyNumber(128); // Doesn't panic when 128 > 100
}

There are a few considerations to make, and I've attempted different solutions. I'll avoid generic programming for the examples of them below:

  • Basing the type off enum ensures that only valid values are possible values. This becomes messy very fast:

    enum MyNumber {
        One,
        Two,
        ...
    }
    impl MyNumber {
        fn convert(i8) -> MyNumber {
            match {
                1 => MyNumber::One,
                2 => MyNumber::Two,
                ...
            }
        }
    }
    
  • Expose a method which checks parameters before setting the fields, the textbook associated function. This doesn't prevent assigning using the struct constructor.

  • Validate operands (and forcibly rectify them) whenever an operation occurs. This seems reasonable, but requires each method to repeat the validation code.

    extern crate num;
    
    use num::Bounded;
    use std::cmp;
    struct MyNumber {
        val: i8,
    }
    
    impl Bounded for MyNumber {
        fn max_value() -> Self {
            MyNumber { val: 65 }
        }
        fn min_value() -> Self {
            MyNumber { val: -50 }
        }
    }
    impl MyNumber {
        fn clamp(&mut self) {
            self.val = cmp::min(MyNumber::max_value().val, 
                                cmp::max(MyNumber::min_value().val, self.val))
        }
        fn add(&mut self, mut addend: Self) {
            self.clamp();
            addend.clamp(); 
            //TODO: wrap or saturate result
            self.val = self.val + addend.val
        }
    }
    
    fn main() {
        let mut a = MyNumber { val: i8::max_value() };
        let b = MyNumber { val: i8::min_value() };
        a.add(b);
        println!("{} + {} = {}",
                 MyNumber::max_value().val,
                 MyNumber::min_value().val, 
                 a.val);
    }
    

None of the solutions above are very elegant - to some degree this is because they are prototype implementations. There must be a cleaner way to limit the domain of a numeric type!

What combination of type and traits would check bounds, use them for modular/saturation arithmetic, and easily convert to a numeric primitive?

EDIT: This question has been flagged as a duplicate of a much older question from 2014. I do not believe the questions are the same on the grounds that Rust was pre alpha and major improvements to the language were brought with version 1.0. The difference is of a greater scale than that between Python 2 and 3.

Forgiven answered 8/1, 2017 at 6:32 Comment(5)
Even if something major enough was changed or added to Rust, it would still be appropriate to add those answers to the existing question. Otherwise every single question on Stack Overflow would need to be re-asked every few months on the off chance that something had changed with the surrounding environment. The answer you received here echoes the pre-existing answer (down to the constructor and trait implementations), so I haven't seen a reason they aren't duplicates. There's also the bounty avenue if you believe an older question needs more attention.Paulsen
@Paulsen Thank you for the clarification. That is a fair perspective on the matter.Forgiven
No worries! The broader community might still disagree with me and vote to reopen; I'm just trying to do my part in keeping things tidy. ^_^Paulsen
@Forgiven No version related tag for rust?Stendhal
@MYGz Though they exist and I think there are other factors diminishing the amount of version tags (rust-0.8, rust-0.9, rust-0.11), your implied point is fair. At the moment it seems "rust" is 1.0+ and everything prior is not-quite-rust. But this is the first time I've observed a young language becoming de facto, so I don't know to what extent the terminology -- or need to differentiate -- has solidified.Forgiven
F
8

Expose a method which checks parameters before setting the fields, the textbook associated function. This doesn't prevent assigning using the struct constructor.

It does if the field is private.

In Rust, functions in the same module, or submodules, can see private items... but if you put the type into its own module, the private fields are not available from outside:

mod mynumber {
    // The struct is public, but the fields are not.
    // Note I've used a tuple struct, since this is a shallow
    // wrapper around the underlying type.
    // Implementing Copy since it should be freely copied,
    // Clone as required by Copy, and Debug for convenience.
    #[derive(Clone,Copy,Debug)]
    pub struct MyNumber(i8);

And here's a simple impl with a saturating add, which leverages i8's built in saturating_add to avoid wrapping so that simple clamping works. The type can be constructed using the pub fn new function, which now returns an Option<MyNumber> since it can fail.

    impl MyNumber {
        fn is_in_range(val: i8) -> bool {
            val >= -100 && val <= 100
        }
        fn clamp(val: i8) -> i8 {
            if val < -100 {
                return -100;
            }
            if val > 100 {
                return 100;
            }
            // Otherwise return val itself
            val
        }
        pub fn new(val: i8) -> Option<MyNumber> {
            if MyNumber::is_in_range(val) {
                Some(MyNumber(val))
            } else {
                None
            }
        }

        pub fn add(&self, other: MyNumber) -> MyNumber {
            MyNumber(MyNumber::clamp(self.0.saturating_add(other.0)))
        }
    }
}

Other modules can use the type:

use mynumber::MyNumber;

And some example uses:

fn main() {
    let a1 = MyNumber::new(80).unwrap();
    let a2 = MyNumber::new(70).unwrap();
    println!("Sum: {:?}", a1.add(a2));
    // let bad = MyNumber(123); // won't compile; accessing private field
    let bad_runtime = MyNumber::new(123).unwrap();  // panics
}

Playground

In a more complete implementation I would probably implement std::ops::Add etc. so that I could use a1 + a2 instead of calling named methods.

Futures answered 8/1, 2017 at 9:51 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.