How can I create a trait with specializations passed explicitly?

I'm using the eval library to evaluate some expressions. I want to cast/convert the final outcome of the expressions to a specific type I provide in a generic parameter. I'm a C++ dev originally, so here's the outcome I'm looking for. Imagine SomeClass is the expression class I have:

#include <iostream>
#include <type_traits>
#include <string>

struct SomeClass
{
    std::string someData;
};

// I need a trait that can be specialized as follows
// NOTE: this is not about strings at all! This is just an example!
template <typename T>
T ConvertToType(const SomeClass& c);

template <>
uint64_t ConvertToType(const SomeClass& c)
{
    return std::stoull(c.someData);
}

template <>
uint32_t ConvertToType(const SomeClass& c)
{
    return std::stoul(c.someData);
}

template <>
int64_t ConvertToType(const SomeClass& c)
{
    return std::stoll(c.someData);
}

template <>
int32_t ConvertToType(const SomeClass& c)
{
    return std::stoi(c.someData);
}

int main()
{
    SomeClass c{"1"};
    auto a1 = ConvertToType<uint64_t>(c);
    static_assert (std::is_same<decltype(a1), uint64_t>::value, "");
    auto a2 = ConvertToType<uint32_t>(c);
    static_assert (std::is_same<decltype(a2), uint32_t>::value, "");
    auto a3 = ConvertToType<int64_t>(c);
    static_assert (std::is_same<decltype(a3), int64_t>::value, "");
    auto a4 = ConvertToType<int32_t>(c);
    static_assert (std::is_same<decltype(a4), int32_t>::value, "");
    return 0;
}

I've tried to do the same with rust, with this:

trait EvaluateExpression<T> {
    fn eval_expr_to_type(val: &eval::Expr) -> Result<T, Box<dyn std::error::Error>>;
}

impl<T> EvaluateExpression<T> for f64 {
    fn eval_expr_to_type(formula: &eval::Expr) -> Result<T, Box<dyn std::error::Error>> {
        let r1 = formula.exec()?;
        let r2 = r1.as_f64();
        if let Some(res) = r2 {
            Ok(res as T) // problem is here
        } else {
            let error_str = format!("Failed to convert formula {:?} to a number", r2);
            Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, error_str)))
        }
    }
}

This gives me the error:

error[E0605]: non-primitive cast: `f64` as `T`
   --> main.rs:343:16
    |
343 |             Ok(res as T)
    |                ^^^^^^^^
    |
    = note: an `as` expression can only be used to convert between primitive types. Consider using the `From` trait

The problem is when I need to convert the final outcome of the formula (which is f64 in this case) to any other type I want. This explicit casting is desired. For example, if the formula returns an integer, I would like to be able to get it as a float, or double, or any other primitive type I want.

How can I do this in rust?

Well what do you expect res as T to do? You specified no bounds on T, so it could be anything from strings to thread handles or open files.

One way to fix this is to restrict the allowed types T, e.g. you can require that you can convert an f64 into a T using the Into trait:

impl<T> EvaluateExpression<T> for f64
where
    f64: Into<T>,
{
    fn eval_expr_to_type(formula: &eval::Expr) -> Result<T, Box<dyn std::error::Error>> {
        let r1 = formula.exec()?;
        let r2 = r1.as_f64();
        if let Some(res) = r2 {
            Ok(res.into())
        } else {
            let error_str = format!("Failed to convert formula {:?} to a number", r2);
            Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, error_str)))
        }
    }
}

Note that the chosen format is a bit weird — calling f64::eval_expr_to_type(...) does not produce an f64, rather it produces some other type T that an f64 can be turned into. You may want to consider this instead:

trait EvaluateExpression {
    fn eval_expr_to_type(val: &eval::Expr) -> Result<Self, Box<dyn std::error::Error>>;
}
impl EvaluateExpression for f64 {
    fn eval_expr_to_type(formula: &eval::Expr) -> Result<f64, Box<dyn std::error::Error>> {
        let r1 = formula.exec()?;
        let r2 = r1.as_f64();
        if let Some(res) = r2 {
            Ok(res)
        } else {
            let error_str = format!("Failed to convert formula {:?} to a number", r2);
            Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, error_str)))
        }
    }
}

Thank you for the response. This function will be required to return many different types. So imagine it getting copied multiple times, and returning f32, i64, i32, u64, u32 as well. Will that work there?

In my case, I tried copying this function and changing it to:

impl<T> EvaluateExpression<T> for f32
where
    f32: Into<T>,
{ 
// ...
}

But this is causing a compilation error:

error[E0277]: the trait bound `T: std::convert::From<f64>` is not satisfied
   --> main.rs:362:16
    |
356 |     f32: Into<T>,
    |                  - help: consider further restricting type parameter `T`: `, T: std::convert::From<f64>`
...
362 |             Ok(res.into())
    |                ^^^^^^^^^^ the trait `std::convert::From<f64>` is not implemented for `T`
    |
    = note: required because of the requirements on the impl of `std::convert::Into<T>` for `f64`

Is your solution only for one type?

You forgot to change the return type to f32. Note that you can also use Self instead of the actual type name. You may want to consider moving the generics around so you can put the exec() logic in a single generic method, and put some trait on the decodable types instead.

I'm sorry, I'm having trouble getting the solution to work. I copied this from your code and added the f32 version, but it still gives an error:

trait EvaluateExpression {
    fn eval_expr_to_type(val: &eval::Expr) -> Result<Self, Box<dyn std::error::Error>>;
}

impl EvaluateExpression for f64 {
    fn eval_expr_to_type(formula: &eval::Expr) -> Result<f64, Box<dyn std::error::Error>> {
        let r1 = formula.exec()?;
        let r2 = r1.as_f64();
        if let Some(res) = r2 {
            Ok(res)
        } else {
            let error_str = format!("Failed to convert formula {:?} to a number", r2);
            Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, error_str)))
        }
    }
}

impl EvaluateExpression for f32 {
    fn eval_expr_to_type(formula: &eval::Expr) -> Result<f32, Box<dyn std::error::Error>> {
        let r1 = formula.exec()?;
        let r2 = r1.as_f64();
        if let Some(res) = r2 {
            Ok(res)
        } else {
            let error_str = format!("Failed to convert formula {:?} to a number", r2);
            Err(Box::new(std::io::Error::new(std::io::ErrorKind::InvalidData, error_str)))
        }
    }
}

This gives the error:

error[E0277]: the size for values of type `Self` cannot be known at compilation time
   --> main.rs:335:5
    |
335 |     fn eval_expr_to_type(val: &eval::Expr) -> Result<Self, Box<dyn std::error::Error>>;
    |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^-
    |     |                                                                                 |
    |     |                                                                                 help: consider further restricting `Self`: `where Self: std::marker::Sized`
    |     doesn't have a size known at compile-time
    |
    = help: the trait `std::marker::Sized` is not implemented for `Self`
    = note: to learn more, visit <https://doc.rust-lang.org/book/ch19-04-advanced-types.html#dynamically-sized-types-and-the-sized-trait>
    = note: required by `std::result::Result`

Ah, just follow the suggestion of the compiler:

trait EvaluateExpression {
    fn eval_expr_to_type(val: &eval::Expr) -> Result<Self, Box<dyn std::error::Error>>
    where
        Self: Sized;
}

Since I don't have your entire code, I can't test it locally, so I may miss some detail.

Ah, sorry about that.

One last question. With this setting, how can I call the function in the trait with a specific type? There's no generic parameter anymore. How can I call it once for f64 and once for f32?

You can do either explicitly ask for the method on a specific type:

let a = f32::eval_expr_to_type(...).unwrap();

or let type inference figure out which type the trait needs to be called for:

let a: f32 = EvaluateExpression::eval_expr_to_type(...).unwrap();

Thanks a lot :smile:

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.