简体   繁体   中英

Newtype as generic parameter in Rust?

Suppose I have the following newtype:

pub struct Num(pub i32);

Now, I have a function which accepts an optional 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
}

What I want to write is a generic extract function like the one below (which won't compile):

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

So that I can bypass the match inside my calc function:

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

How can I write extract ?

Motivation: in the program I'm writing, there are several newtypes like Num , and they wrap i8 , i16 and i32 . And there are many different calc functions. It's getting very repetitive to write all these match es at the begining of each calc function.

Such a function would generally be unsafe since the internals might be private (and hence have restricted access). For example, suppose we have a newtype and implement Drop for it.

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!(),
    // };
}

(playground)

If your function worked, you'd be able to move the inner string out of the newtype. Then when the struct is dropped, it's able to access invalid memory.

Nonetheless, you can write a macro that implements something along those lines. If you try to use the macro on something implementing Drop , the compiler will complain, but otherwise, this should work.

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

(playground)

Having an impl block in the output of the macro doesn't cause any problems since you can have as many impl blocks for a single type as you need (in the original module).

A better API would be to have extract return an option, rather than some meaningless value or panicking. Then the any error can easily be handled by the caller.

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

(playground)

There are two main missing pieces here:

  1. You need to abstract the structure of Num , providing a way to extract the inner value without knowing the outer type.
  2. You need to constrain R to have number-like properties, so that you can express the idea of -1 for it.

The first can be solved by implementing Deref for Num and then using it as a trait bound. This will let you access the "inner" value. There are also other traits that have similar capabilities, but Deref is likely the one you want here:

The second can be solved by implementing the One trait imported from the num-traits crate (to get the idea of a 1 value) and by implementing std::ops::Neg to be able to negate it to get -1 . You will also need to require that R is Copy or Clone so you can move it out of the reference.

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

Depending on how you intend to use this, you might want to get rid of R , since it is always determined by T . As-is, the function is told by the caller the concrete types of T and R , and will make sure that R is T 's deref target. But it might be better if the caller only needs to provide T and let R be deduced from 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(),
    }
}

Turns out I figured out a much easier and elegant way to accomplish this. First, implement Default trait for my newtype:

use std::default::Default;

pub struct Num(pub i32);

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

And then, when needed, just use unwrap_or_default accessing first newtype tuple element:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM