Rust Patterns That Matter #2: From / Into Conversions
Post 2 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Previous: #1: Newtype | Next: #3: Error Handling
You're writing an API that accepts different input types. Some callers have a
&str, some have a String, some have a
PathBuf. You want all of them to work without forcing callers to
convert manually. The From / Into traits are how
idiomatic Rust APIs achieve this.
The motivation
struct Email {
address: String,
}
impl Email {
fn new(address: String) -> Self {
Email { address }
}
}
// Caller with a String: fine
let e = Email::new("a@b.com".to_string());
// Caller with a &str: has to convert manually
let e = Email::new("a@b.com".to_string()); // verbose
Every caller has to write .to_string(). It's small, but it's friction.
In an API with many such functions, the friction adds up.
The pattern: From<T>
Implement From for each type you want to accept:
impl From<String> for Email {
fn from(address: String) -> Self {
Email { address }
}
}
impl From<&str> for Email {
fn from(address: &str) -> Self {
Email { address: address.to_string() }
}
}
Now callers can use .into():
let e: Email = "a@b.com".into();
let e: Email = String::from("a@b.com").into();
The blanket impl
When you implement From<T> for U, the standard library
automatically provides Into<U> for T via a blanket impl.
You never need to implement Into directly. Always implement
From.
Using impl Into<T> in function signatures
The real payoff is in function parameters:
fn send_email(to: impl Into<Email>, body: impl Into<String>) {
let to = to.into();
let body = body.into();
// ...
}
// All of these work:
send_email("a@b.com", "Hello");
send_email(Email::from("a@b.com"), String::from("Hello"));
send_email("a@b.com", "Hello".to_string());
The caller passes whatever type they have. The conversion happens inside the function. No ceremony at the call site.
Error handling: From powers ?
The ? operator uses From to convert errors. When you write:
fn read_config() -> Result<Config, MyError> {
let text = std::fs::read_to_string("config.toml")?;
let config: Config = toml::from_str(&text)?;
Ok(config)
}
The first ? might produce an io::Error. The second might
produce a toml::de::Error. The function returns MyError.
The ? operator calls From::from() to convert each error
type into MyError. If you've implemented
From<io::Error> for MyError and
From<toml::de::Error> for MyError, it just works.
This is the foundation of the error handling pattern in
#3. thiserror
generates these From impls for you.
TryFrom / TryInto
For conversions that can fail, use TryFrom:
struct Port(u16);
impl TryFrom<u32> for Port {
type Error = String;
fn try_from(value: u32) -> Result<Self, Self::Error> {
if value > 65535 {
Err(format!("{value} is not a valid port"))
} else {
Ok(Port(value as u16))
}
}
}
let p: Result<Port, _> = 8080_u32.try_into(); // Ok(Port(8080))
let p: Result<Port, _> = 99999_u32.try_into(); // Err("99999 is not a valid port")
When to use it
- Public API boundaries: accept
impl Into<String>instead of forcing callers to convert - Error type conversions: implement
From<SourceError>for your error type so?converts automatically - Newtype wrappers:
From<u64> for UserIdfor ergonomic construction - Fallible conversions:
TryFromfor parsing, range checking, validation
When not to: if the conversion is lossy, surprising, or has side effects, make it
an explicit named method. From / Into imply a natural,
lossless conversion. If .into() would silently truncate data or change
semantics, use a named method instead so the caller knows what's happening.
See it in practice: Building a Chat Server #1: Hello, TCP uses this pattern for message parsing and error conversion.
Telex