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

В 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 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 был pre alpha, а в версии 1.0 были внесены значительные улучшения в язык. Разница в большем масштабе, чем между Python 2 и 3.


person Aaron3468    schedule 08.01.2017    source источник
comment
Даже если что-то достаточно важное было изменено или добавлено в Rust, все равно было бы уместно добавить эти ответы к существующему вопросу. В противном случае каждый вопрос о переполнении стека нужно было бы повторно задавать каждые несколько месяцев на случай, если что-то изменилось в окружающей среде. Ответ, который вы здесь получили, перекликается с ранее существовавшим ответом (вплоть до реализаций конструктора и трейта), поэтому я не видел причины, по которым они не дублируются. Если вы считаете, что более старый вопрос требует большего внимания, есть еще один вариант вознаграждения.   -  person Shepmaster    schedule 08.01.2017
comment
@Shepmaster Спасибо за разъяснения. Это честная точка зрения по данному вопросу.   -  person Aaron3468    schedule 09.01.2017
comment
Не стоит беспокоиться! Более широкое сообщество может все еще не согласиться со мной и проголосовать за повторное открытие; Я просто пытаюсь внести свой вклад в поддержание порядка. ^ _ ^   -  person Shepmaster    schedule 09.01.2017
comment
@ Aaron3468 Нет тега, связанного с версией для ржавчины?   -  person Mohammad Yusuf    schedule 09.01.2017
comment
@MYGz Хотя они существуют, и я думаю, что есть и другие факторы, уменьшающие количество тегов версий (rust-0.8, rust-0.9, rust-0.11), ваша подразумеваемая точка является справедливой. На данный момент кажется, что ржавчина 1.0+, а все предыдущее не совсем ржавое. Но я впервые наблюдаю, как молодой язык становится де-факто, поэтому я не знаю, в какой степени терминология - или необходимость дифференциации - укрепилась.   -  person Aaron3468    schedule 10.01.2017


Ответы (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 вместо вызова именованных методов.

person Chris Emerson    schedule 08.01.2017