Как создать типизированный числовой тип с ограниченным диапазоном?

В Rust мне нужен числовой тип со свойством симметричного домена около 0. Если число n является допустимым значением, то число -n также должно быть допустимым. Как бы я обеспечил безопасность типов во время инициализации и арифметики? Как лучше всего реализовать модульную арифметику и насыщенность на типе?


Простейший пример проблемы:

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

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

Есть несколько соображений, и я пытался найти разные решения. Я буду избегать общего программирования для приведенных ниже примеров:

  • Основываясь на типе enum гарантирует, что только допустимые значения являются возможными значениями. Это становится очень грязным:

    enum MyNumber {
        One,
        Two,
        ...
    }
    impl MyNumber {
        fn convert(i8) -> MyNumber {
            match {
                1 => MyNumber::One,
                2 => MyNumber::Two,
                ...
            }
        }
    }
    
  • Выставьте метод, который проверяет параметры перед настройкой полей, связанные с учебником функции. Это не мешает назначению с использованием конструктора структуры.

  • Проверяйте операнды (и принудительно исправляйте их) всякий раз, когда происходит операция. Это кажется разумным, но требует, чтобы каждый метод повторял код проверки.

    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);
    }
    

Ни одно из приведенных выше решений не является очень элегантным - в некоторой степени это связано с тем, что они являются прототипными реализациями. Должен быть более чистый способ ограничения домена числового типа!

Какая комбинация типа и свойств будет проверять границы, использовать их для арифметики модульности / насыщенности и легко конвертировать в числовой примитив?

РЕДАКТИРОВАТЬ: Этот вопрос был помечен как дубликат гораздо более старого вопроса 2014 года. Я не верю, что вопросы совпадают на том основании, что Rust был пре-альфа, и в версию 1.0 были внесены значительные улучшения в язык. Разница в большем масштабе, чем между Python 2 и 3.

1 ответ

Решение

Выставьте метод, который проверяет параметры перед настройкой полей, связанные с учебником функции. Это не мешает назначению с использованием конструктора структуры.

Это делает, если поле является частным.

В Rust функции в том же модуле или подмодулях могут видеть приватные элементы... но если вы поместите тип в его собственный модуль, приватные поля будут недоступны извне:

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

А вот простой impl с насыщающей добавкой, которая использует i8 построен в saturating_add чтобы избежать обмотки, чтобы простой зажим работал. Тип может быть создан с использованием pub fn new функция, которая теперь возвращает Option<MyNumber> так как это может потерпеть неудачу.

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

Другие модули могут use тип:

use mynumber::MyNumber;

И некоторые примеры используют:

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
}

Детская площадка

В более полной реализации я бы, вероятно, реализовал std::ops::Add и т.д., чтобы я мог использовать a1 + a2 вместо вызова именованных методов.

Другие вопросы по тегам