(Pre-)RFC: Deprecate FromStr in favor of TryFrom<&str>

I rushed things a bit and prematurely created a PR draft. However this needs plenty of discussion so I'm creating this topic.

I feel like FromStr — and consequently str::parse() — should be deprecated for the following reasons:

  • TryFrom<&str> and From<&str> virtually serves the same purpose as FromStr
  • From<&str> is much more idiomatic than FromStr when the conversion is infallible:
    struct Dummy(String);
    
    impl std::str::FromStr for Dummy {
        type Err = core::convert::Infallible;
        
        fn from_str(s: &str) -> Result<Self, Self::Err> {
            Ok(Dummy(s.to_owned()))
        }
    }
    
    //vs
    
    impl From<&str> for Dummy {
    
        fn from(s: &str) -> Self {
            Dummy(s.to_owned())
        }
    }
    
  • FromStr limits lifetimes in a way that prevents borrowing the passed string:
    struct Dummy<'a>(&'a str);
    
    // This doesn't compile
    impl<'a> std::str::FromStr for Dummy<'a> {
        type Err = core::convert::Infallible;
        
        fn from_str(s: &str) -> Result<Self, Self::Err> {
            Ok(Dummy(s))
        }
    }
    
    // This works
    impl<'a> From<&'a str> for Dummy<'a> {
    
        fn from(s: &'a str) -> Self {
            Dummy(s)
        }
    }
    

In the premature RFC I posted some concerns were raised about the churn created by this deprecation as FromStr is widely used.

4 Likes

As I said on the PR, I am strongly opposed to deprecating parse. It is ubiquitous in Rust code, and the churn it would cause is mind boggling, especially since it still works just fine. Its main problem is that it is a bit redundant. I personally don't feel like that rises to level of deprecating it.

Deprecating FromStr wouldn't be as bad as deprecating parse (if that's even possible), but it is still widely used, and as with parse, works just fine.

Overall, I'd like to see fewer deprecations in general, or at the very least, group them together so that they can all be addressed at once instead of spreading them out. But this particular deprecation doesn't seem well motivated IMO.

7 Likes

Perhaps we could start with a blanket impl<T> FromStr for T where T: TryFrom<&str>? We could keep parse() while at the same time guiding developers towards the more general TryFrom trait.

Personally I'm not as a big a fan of parse() since I feel it is pretty hard to understand how that works if you're encountering as a new user. By comparison, I feel like try_into() generalizes better and makes it slightly clearer what's going on under the covers.

7 Likes

I'm pretty sure that's potentially unsound because of lifetime specializations (any specialization only using a trait bound is potentially unsound). This means we can't expose it in a public api, because specialization is likely to change in significant ways to fix this soundness hole. (Also, we don't expose unstable features, like specialization in public apis, i.e. it should be possible to remove all uses of specialization without breaking anyone's builds).

2 Likes

One thing I love about parse() is it's an inherent method and therefore you don't need any traits in scope to use it.

At present TryInto is not in the prelude, so using it for parsing means you have to import it every time.

This makes the most sense to me. I'd suggest not deprecating parse at least until when/if TryInto winds up in a future (edition) prelude and can then be used as easily as parse() can today.

3 Likes

In the current libstd, .try_into() and .parse() are different for how they should work.

.try_into(), as a fallible version of .into(), mostly transforms the "shape" of the value to fit another type.

.parse() can be considered the reverse of .to_string(), will read the string content to reconstruct a value in the target type.

Additionally, TryFrom must satisfy this relationship, which is not bound for FromStr:

  • T: From<S>T: TryFrom<S, Error=!>

As a concrete example:

use std::convert::TryInto;
use serde_json::{Value, json}; 

fn main() {
    let a: Value = "[1, 2, 3]".try_into().unwrap();
    let b: Value = "[1, 2, 3]".parse().unwrap();
    assert_eq!(dbg!(a), json!("[1, 2, 3]"));
    assert_eq!(dbg!(b), json!( [1, 2, 3] ));
}
13 Likes

One problem that I haven't seen mentioned is that TryFrom doesn't get to use deref coercion the same way FromStr can due to the lack of an explicit receiving type. Using TryFrom exclusively would require a lot of additions of as_ref() or &* to code that otherwise just works via FromStr.

5 Likes