2

I wish to define a function that inspects two values and optionally changes one of them into a matching type. The motivation is to perform automatic casting for math formulas. My rules would be:

  • Both are i64, leave alone
  • Both are f64, leave alone
  • One is i64, one is f64, change the i64 to f64
  • Other rules for other types

For example:

fn normalize(arg1: Option<MyValue>, arg2: Option<MyValue>) -> (Option<MyValue>, Option<MyValue>) {
    unimplemented!();
}

My example will return a tuple with the optionally transformed values. MyValue does not implement Copy, but it does implement Clone. It is an enum which can hold an integer or a rational or a string, etc.

The alternatives I can see are:

  1. Return a tuple. Transform the value that needs to change, clone the other to avoid borrow-checker. Conversion failures come back as None.
  2. Return a tuple. Transform the value that needs to change, return the other one unchanged, figure out how to move the value.
  3. Return (). Make the parameters &mut. Change the one that needs to change, if any.
  4. Some other way that I do not know about, because I am new to Rust.

Which approach is most idiomatic to Rust? If I do not clone, how do I notate the signature to placate the borrow-checker?

My real enum is:

#[derive(Clone, PartialEq, Debug)]
pub enum ShyScalar {
    Boolean(bool),
    Integer(i64),
    Rational(f64),
    String(String),
    Error(String)
}
pretzelhammer
  • 13,874
  • 15
  • 47
  • 98
Paul Chernoch
  • 5,275
  • 3
  • 52
  • 73
  • Can you make an enum that includes all of your acceptable types, and then a conversion operator on that enum which normalizes? – tadman Jun 14 '19 at 19:03
  • For simplicity, I did not show an extra parameter, the operator. The conversions are not the same for each operator. Think of arg1 as the left value, arg2 is the right value, and there is a binary operator between them. This normalization function will be attached to the operator Enum. – Paul Chernoch Jun 14 '19 at 19:23
  • *Conversion failures ...* Like what? – Shepmaster Jun 14 '19 at 19:29
  • This question is opinion-based and I've voted to close it as such. Pick one and try it. If it works, great. If it doesn't or it's annoying to use, pick another. If you exhaust your list of options, ask a new question looking for alternatives. – Shepmaster Jun 14 '19 at 19:31
  • Because this question in its current form appears off-topic, you may wish to look at other resources. More open-ended questions and discussions are welcome on [the Rust subreddit](https://www.reddit.com/r/rust/), [the Rust users forum](https://users.rust-lang.org/), or [the Rust Discord server](https://www.rust-lang.org/community). – Shepmaster Jun 14 '19 at 19:31
  • My personal **opinion** is to use `&mut`, but see [Change enum variant while moving the field to the new variant](https://stackoverflow.com/q/36557412/155423) for a reason you'd need to take by value. – Shepmaster Jun 14 '19 at 19:32

1 Answers1

0

You can think of a trait with an associated type as a compile-time function that maps between types. For example:

trait MapType {
    type Output;
}

impl MapType for f64 {
    type Output i64;
}

impl MapType for bool {
    type Output u8;
}

For each type that you might need, you can implement MapType to provide a mapping to a unique Output type.

Your case is about pairs of types, and you can extend the idea above by adding a parameter:

trait Normalize<T>: Sized {
    type Norm;
}

Each implementation of Normalize produces a unique Norm type, for the combination of the two types, Self and T.

But you will also need some constraints; after all, you will need to be able to actually convert between these types. And if a number is too big to be converted, the conversion will fail, so you will need the extra type constraints, TryFrom and TryInto, to express which things can be converted into what:

use std::convert::{TryFrom, TryInto};

trait Normalize<T>: Sized
where
    T: TryInto<Self::Norm>,
{
    type Norm: TryFrom<Self>;
}

Implement it for the pairs of types you want:

impl Normalize<u32> for f64 {
    type Norm = f64;
}

impl Normalize<f64> for u32 {
    type Norm = f64;
}

And also for all pairs of the same type:

impl<X> Normalize<X> for X {
    type Norm = X;
}

You can then implement normalize like this:

fn normalize<A, B>(arg1: Option<A>, arg2: Option<B>) -> (Option<A::Norm>, Option<A::Norm>) 
where
    A: Normalize<B>,
    A::Norm: TryFrom<B>,
{
    (
        arg1.and_then(|a| a.try_into().ok()),
        arg2.and_then(|b| b.try_into().ok())
    )
}

fn main() {
    println!("{:?}", normalize(Some(1u32), Some(1u32))); // (Some(1), Some(1))
    println!("{:?}", normalize(Some(1f64), Some(1u32))); // (Some(1.0), Some(1.0))
    println!("{:?}", normalize(Some(1u32), Some(1f64))); // (Some(1.0), Some(1.0))
    println!("{:?}", normalize(Some(1f64), Some(1f64))); // (Some(1.0), Some(1.0))
}

You will quickly realise that I have cheated a little here, by using u32 instead of u64. That's because there is no TryFrom<u64> implementation for f64. If you need these pairs of types then you can still keep the same approach as I have outlined, but you would need to define your own version of the TryInto and TryFrom traits and implement those yourself for all the types pairs that you need.

Peter Hall
  • 53,120
  • 14
  • 139
  • 204