Converting &str that contains a decimal or hex representation to a decimal in a generic way

I was trying to write a generic function that could take a &str containing either a decimal representation 123, or a hexadecimal representation 0xFF and parse this into a decimal number.

use num_traits::Num;

fn parse<T>(value: &str) -> T
where T: Num + FromStr, <T as std::str::FromStr>::Err: std::fmt::Debug {
    let value = value.trim();
    if value.starts_with("0x") {
        <T>::from_str_radix(value.strip_prefix("0x").unwrap(), 16).ok().unwrap()
    } else {
        value.parse::<T>().unwrap()
    }
}

I didn't really have any idea what i was doing, i do understand the basics of traits and generics, but this was way out of my league. So i have some questions about this:

what does FromStr, <T as std::str::FromStr>::Err: std::fmt::Debug actually mean? i have only seen syntax like <T: Foo + Bar> but i don't really understand why all that is needed? and i have never seen this type of syntax in generic functions before.

Also when i remove the return type of T and try to return a Result<T, Box<dyn Error>> and then remove all the unwrap and ok and instead use the ?

fn parse<T>(value: &str) -> Result<T, Box<dyn Error>>
where T: Num + FromStr, <T as std::str::FromStr>::Err: std::fmt::Debug {
    let value = value.trim();
    if value.starts_with("0x") {
        <T>::from_str_radix(value.strip_prefix("0x").unwrap(), 16)?
    } else {
        value.parse::<T>()?
    }
}

i get this error from the compiler

try expression alternatives have incompatible types
       expected enum `std::result::Result<T, Box<(dyn std::error::Error + 'static)>>`
found type parameter `T`rustcE0308
options.rs(51, 14): this type parameter
options.rs(55, 13): try using a variant of the expected enum

Which i as a beginner have a really hard time understanding, it looks like it still wants to return the T type?

Each FromStr implementation has an associated Err type that it returns on failure. You are calling Result::unwrap, which requires errors to implement the Debug trait so it can print them to the console if it panics.

You are trying to return a T, but you need to wrap it in Ok() to return a Result<T, _>.

    if value.starts_with("0x") {
        Ok(<T>::from_str_radix(value.strip_prefix("0x").unwrap(), 16)?)
    } else {
        Ok(value.parse::<T>()?)
    }
1 Like

This means that the T parameter's FromStr impl's associated Err type must implement the Debug trait.

Most error types need debug and display impls in order to be printed out when they are displayed.

Also when i remove the return type of T and try to return a Result<T, Box<dyn Error>> and then remove all the unwrap and ok and instead use the ?

You're misunderstanding what ? does. It is a replacement for unwrap, in that it takes it's value out of the Result container when the result is Ok. So you never want to use it on the last expression in a function that returns result, because it converts Result<T, ..> into T. It returns Errs but unwraps Oks. So you either need to add Ok or use map_err instead.

Now, if you make that change you're still going to get an error:

  = note: expected enum `std::result::Result<_, Box<(dyn std::error::Error + 'static)>>`
             found enum `std::result::Result<_, Box<<T as Num>::FromStrRadixErr>>`
help: consider constraining the associated type `<T as Num>::FromStrRadixErr` to `(dyn std::error::Error + 'static)`

The problem here being that FromStrRadixErr is an associated type on Num that doesn't have any bounds that would require it to implement std::error::Error. So in order to box it up, you need to add that constraint:

use num_traits::Num;
use std::str::FromStr;
use std::error::Error;

fn parse<T>(value: &str) -> Result<T, Box<dyn Error>>
where
    T: Num + FromStr,
    <T as Num>::FromStrRadixErr: Error + 'static,
    <T as FromStr>::Err: Error + 'static,
{
    let value = value.trim();
    if value.starts_with("0x") {
        Ok(<T>::from_str_radix(value.strip_prefix("0x").unwrap(), 16)?)
    } else {
        Ok(value.parse::<T>()?)
    }
}
1 Like

Thank you so much to you both for your excellent answers.

I'm starting to understand the errors a lot clearer now, didn't understand that the they where referring to the traits respective associated types.

I'm still a but confused over some things:

  • the comma syntax after the traits , does this sort of refer to the Result<T, Err>? because i have never seen a comma in a generic declaration before.

  • Why do we need i need to add + 'static to the associated error types? I know static means that something will exist for the entire duration of the programs lifetime. But whats its purpose in this context?

sorry for my questions... but i find this very fascinating and interesting

The where keyword can be followed by any number of Type: Bound1 + Bound2 + Bound3 clauses, separated by commas. All of these clauses must be satisfied in order for the entire where clause to be satisfied.

A simpler example would be a function like this (a simplified version of std::io::copy):

fn copy<R, W>(reader: R, writer: W)
where
    R: Read,
    W: Write,
{
    // ...
}

This says the type R must implement the Read trait, and the type W must implement the Write trait.

The 'static bound means that the error can't contain any non-'static references, or in other words it can't be restricted to some local stack frame. It shows up here because Box<dyn Error> is shorthand for Box<dyn Error + 'static>. This shorthand is called default trait object lifetimes.

If that explanation doesn't immediately make sense, don't worry: This is getting pretty technical. At a higher level, you can read a 'static bound as "this type must own its data, not borrow it." And it's required by default for Box<dyn Trait> because Box is mostly useful with owning types.

2 Likes

In addition to this:

It might help to envision that we could box data that does somehow reference data on the stack; for example, if your error types borrowed from local strings, yet you still wanted to construct trait objects to dynamically wrap up multiple error types. We could generalize your function to allow that by replacing the 'static bounds with a generic lifetime parameter:

fn parse<'a, T>(value: &str) -> Result<T, Box<dyn Error + 'a>>
where
    T: Num + FromStr,
    <T as Num>::FromStrRadixErr: Error + 'a,
    <T as FromStr>::Err: Error + 'a,
{
    let value = value.trim();
    if value.starts_with("0x") {
        Ok(<T>::from_str_radix(value.strip_prefix("0x").unwrap(), 16)?)
    } else {
        Ok(value.parse::<T>()?)
    }
}

In practice, this isn't really all that valuable, because as mentioned, usually we box things in order to model owned data, and not merely to move borrowed data onto the heap.

Okey, it's getting clearer.

So to sum up:

fn parse<'a, T>(value: &str) -> Result<T, Box<dyn Error + 'a>>
where
    T: Num + FromStr,
    <T as Num>::FromStrRadixErr: Error + 'a,
    <T as FromStr>::Err: Error + 'a,

So what we are basically saying is:

  • T must implement the Num trait, and the FromStr trait
  • The associated type FromStrRadixErr on the implemented Num trait on T must implement the Error trait and ether own its own data, or have a generic lifetime parameter so we don't risk its data to go out of scope.
  • The associated type Err on the FromStr trait implemented on T must implement the Error type and also must own it's data or have a generic lifetime param so its data doesn't risk going out of scope.

Man... so much to think about, but interesting.

1 Like