Newtype как общий параметр в Rust? - PullRequest
0 голосов
/ 30 октября 2019

Предположим, у меня есть следующий новый тип:

pub struct Num(pub i32);

Теперь у меня есть функция, которая принимает необязательный Num:

pub fn calc(nu: Option<Num>) -> i32 {
    let real_nu = match nu { // extract the value inside Num
        Some(nu) => nu.0,
        None     => -1
    };
    // performs lots of complicated calculations...
    real_nu * 1234
}

То, что я хочу написать, является универсальнымextract функция, подобная приведенной ниже (которая не будет компилироваться):

// T here would be "Num" newtype
// R would be "i32", which is wrapped by "Num"

pub fn extract<T, R>(val: Option<T>) -> R {
    match val {
        Some(val) => val.0, // return inner number
        None      => -1 as R
    }
}

Чтобы я мог обойти match внутри моей функции calc:

pub fn calc(nu: Option<Num>) -> i32 {
    // do a lot of complicated calculations...
    extract(nu) * 1234 // automatically extract i32 or -1
}

Как можноЯ пишу extract?

Мотивация: В программе, которую я пишу, есть несколько новых типов, таких как Num, и они переносят i8, i16 и i32. И есть много разных calc функций. Писать все эти match в начале каждой calc функции становится все более и более повторяющимся.

Ответы [ 3 ]

2 голосов
/ 30 октября 2019

Такая функция, как правило, была бы небезопасна, поскольку внутренние устройства могут быть частными (и, следовательно, иметь ограниченный доступ). Например, предположим, что у нас есть новый тип и для него реализовано Drop.

struct NewType(String);

impl Drop for NewType {
    fn drop(&mut self) {
        println!("{}", self.0)
    }
}

fn main() {
    let x = NewType("abc".to_string());
    let y = Some(x);

    // this causes a compiler error
    // let s = match y {
    //     Some(s) => s.0,
    //     None => panic!(),
    // };
}

(игровая площадка)

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

Тем не менее, вы можете написать макрос, который реализует что-то в этом направлении. Если вы попытаетесь использовать макрос для чего-то, реализующего Drop, компилятор будет жаловаться, но в противном случае это должно сработать.

macro_rules! extract_impl {
    (struct $struct_name: ident($type_name: ty);) => {
        struct $struct_name($type_name);
        impl $struct_name {
            fn extract(item: Option<Self>) -> $type_name {
                match item {
                    Some(item) => item.0,
                    None => panic!(), // not sure what you want here
                }
            }
        }
    };
}

extract_impl! {
    struct Num(i32);
}

impl Num {
    fn other_fun(&self) {}
}

fn main() {
    let x = Num(5);
    println!("{}", Num::extract(Some(x)));
}

(детская площадка)

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

ЛучшеAPI будет extract возвращать опцию, а не какое-то бессмысленное значение или панику. Тогда любая ошибка может быть легко обработана вызывающим абонентом.

macro_rules! extract_impl {
    (struct $struct_name: ident($type_name: ty);) => {
        struct $struct_name($type_name);
        impl $struct_name {
            fn extract(item: Option<Self>) -> Option<$type_name> {
                item.map(|item| item.0)
            }
        }
    };
}

extract_impl! {
    struct Num(i32);
}

impl Num {
    fn other_fun(&self) {}
}

fn main() {
    let x = Num(5);
    println!("{:?}", Num::extract(Some(x)));
}

(детская площадка)

1 голос
/ 30 октября 2019

Здесь есть две основные недостающие части:

  1. Вы должны абстрагировать структуру Num, предоставляя способ извлечения внутреннего значения, не зная внешнего типа.
  2. Вам нужно ограничить R, чтобы иметь числовые свойства, чтобы вы могли выразить идею -1 для него.

Первое можно решить с помощью реализации Deref для Num, а затем использовать его в качестве границы черты. Это позволит вам получить доступ к «внутреннему» значению. Есть и другие черты, которые имеют аналогичные возможности, но, скорее всего, вам нужна Deref:

Вторая проблема может быть решена путем реализации черты One, импортированной из ящика num-traits (чтобы получитьидея значения 1) и реализация std::ops::Neg, чтобы иметь возможность отрицать его, чтобы получить -1. Вам также потребуется, чтобы R было Copy или Clone, чтобы вы могли переместить его из эталона.

use num_traits::One;
use std::ops::{Deref, Neg}; // 0.2.8

pub struct Num(pub i32);

impl Deref for Num {
    type Target = i32;
    fn deref(&self) -> &i32 {
        &self.0
    }
}

pub fn extract<T, R>(val: Option<T>) -> R
where
    T: Deref<Target = R>,
    R: Neg<Output = R> + One + Copy,
{
    match val {
        Some(val) => *val,
        None => -R::one(),
    }
}

В зависимости от того, как вы собираетесь использовать это, вы можете захотетьизбавиться от R, так как оно всегда определяется T. Как есть, эта функция сообщает вызывающей стороне конкретные типы T и R и гарантирует, что R является целью T для разыменования. Но было бы лучше, если бы вызывающему абоненту нужно было только предоставить T и позволить R быть выведенным из T.

pub fn extract<T>(val: Option<T>) -> T::Target
where
    T: Deref,
    <T as Deref>::Target: Neg<Output = T::Target> + One + Copy,
{
    match val {
        Some(val) => *val,
        None => -T::Target::one(),
    }
}
0 голосов
/ 31 октября 2019

Оказывается, я нашел гораздо более простой и элегантный способ сделать это. Сначала реализуйте черту Default для моего нового типа:

use std::default::Default;

pub struct Num(pub i32);

impl Default for Num {
    fn default() -> Self {
        Self(-1)
    }
}

А затем, когда это необходимо, просто используйте unwrap_or_default для доступа к первому элементу кортежа нового типа:

pub fn calc(nu: Option<Num>) -> i32 {
    // do a lot of complicated calculations...
    nu.unwrap_or_default().0 * 1234
}
...