Welcome to The Monthly Oxide, a newsletter where you learn something new about Rust and also get links to neat crates and articles I read this past month. It’s September and let me tell you it’s been a wild ride between closing on a house today and my dog needing surgery because he ate too much grass. I’m hoping this month will be a bit more calmer overall. Today I want to talk about Extension Traits. Have you ever wished you could extend the functionality on std
library types? Maybe you wanted to give another crate’s types a few more methods to use or maybe you just want to split out functionality for a trait that’s OS specific. Well Extension Traits are for you. I think they’re pretty neat when used correctly. Let’s get started!
Extension Traits and Orphan Rules
I’ve talked about the Orphan Rules before on my blog before, but to summarize you can’t implement a foreign trait on a foreign type. Let’s say you’re using a crate and want to implement Debug
for a type in that crate. You would not be able to do it. Debug
is a foreign trait from the std
crate and the type you want to implement it for is foreign since it’s from a crate and not a type you defined, which as you can tell from the article I linked causes a whole lot of issues even if they’re crates you own in the same workspace! If the terminology is confusing “Foreign” is traits or types that exist outside the current crate you are programming and “Local” is types and traits in the current crate you are working on. Here’s a small chart about what kinds of traits you can implement for various types in your own crate:
Allowed
==========================
Foreign Type + Local Trait
Local Type + Local Trait
Local Type + Foreign Trait
Disallowed
==========================
Foreign Type + Foreign Trait
Extension Traits fall into the first two in the allowed list. They are traits you define locally for types either in your crate or another crate. More often than not it’s the first in the list, but sometimes the need for second does arise. A good example is the PermissionsExt
trait for the type std::fs::Permissions
which extends the capability of the type on Unix systems to check the mode of a file, which does not exist on Windows at all. This lets the std
crate use the same type for a concept such as file permissions and then let people opt into functionality based on needs for their program. It’s the second type in the list as both the trait and the type exist locally within the same crate and “extend” the functionality of the type on certain platforms.
That’s good and all, but how would you actually use it? I’ll use an example from work where someone wanted to take a Vec<u8>
or &[u8]
and turn it into an std::net::IpAddr
based on the size: 4 bytes for an IPv4 address and 16 for an IPv6 address. The nice thing would be being able to just call it as if the method took self.
Something like:
let address: IpAddr = bytes.try_into()?;
The problem is that there’s no direct way to do this conversion with what currently exists in the standard library. No problem you say, let’s just implement TryFrom
ourselves! Except you then have this problem:
Compiling playground v0.0.1 (/playground)
error[E0119]: conflicting implementations of trait `std::convert::TryFrom<std::vec::Vec<u8>>` for type `std::net::IpAddr`
--> src/lib.rs:5:1
|
5 | impl TryFrom<Vec<u8>> for IpAddr {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: conflicting implementation in crate `core`:
- impl<T, U> TryFrom<U> for T
where U: Into<T>;
= note: upstream crates may add a new impl of trait `std::convert::From<std::vec::Vec<u8>>` for type `std::net::IpAddr` in future versions
error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
--> src/lib.rs:5:1
|
5 | impl TryFrom<Vec<u8>> for IpAddr {
| ^^^^^----------------^^^^^------
| | | |
| | | `IpAddr` is not defined in the current crate
| | `Vec` is not defined in the current crate
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead
error: aborting due to 2 previous errors
Some errors have detailed explanations: E0117, E0119.
For more information about an error, try `rustc --explain E0117`.
error: could not compile `playground`
To learn more, run the command again with --verbose.
My arch nemesis, the orphan rules. We can solve this with extension traits though! Let’s try the above again like so:
use std::convert::TryInto;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::error::Error;
pub trait IntoAddr {
fn into_addr(self) -> Result<IpAddr, Box<dyn Error>>;
}
impl IntoAddr for &[u8]
{
fn into_addr(self) -> Result<IpAddr, Box<dyn Error>> {
match self.len() {
4 => {
let array: [u8; 4] = self.try_into().unwrap();
Ok(array.into())
},
16 => {
let array: [u8; 16] = self.try_into().unwrap();
Ok(array.into())
}
_ => Err("Can only make an IpAddr from 4 or 16 bytes".to_string().into())
}
}
}
impl IntoAddr for Vec<u8>
{
fn into_addr(self) -> Result<IpAddr, Box<dyn Error>> {
match self.len() {
4 => {
let array: [u8; 4] = self.try_into().unwrap();
Ok(array.into())
},
16 => {
let array: [u8; 16] = self.try_into().unwrap();
Ok(array.into())
}
_ => Err("Can only make an IpAddr from 4 or 16 bytes".to_string().into())
}
}
}
fn main() {
let addr1 = vec![0,0,0,0].into_addr().unwrap();
let addr2 = (&[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]).into_addr().unwrap();
assert_eq!(addr1, IpAddr::V4(Ipv4Addr::new(0,0,0,0)));
assert_eq!(addr2, IpAddr::V6(Ipv6Addr::new(0,0,0,0,0,0,0,0)));
}
In the end we got what we wanted! We define our own trait IntoAddr
and we implement it for &[u8]/Vec<u8>
. With this we can now call into_addr
on the types as if we defined them on the impl for the types ourselves because they take self
as a parameter for the function! While it’s not the standard TryInto
this still lets us use the function calling syntax for a type and we have one location to handle this logic if we do this often enough in the code base. It should be noted you could also just create a function that took these types as input and did this transformation. In many ways it can be a matter of style, but if say you extended types like Option/Result
that often chain multiple functions as combinators, using Extension Traits would be a better choice to fit in with how the type normally works. Like all things this is a tool in your toolbox and how you use it is up to you and your use case! Sometimes just knowing you can do it is super helpful.
That’s it for this month’s article. Relatively short given how busy my life is right now! Next month I might have a longer article or maybe something just as short and sweet. Who knows! I’ll see y’all next month.
Articles
Move Constructors in Rust: Is it possible? - Absolutely bonkers and eye opening article I had seen the other month, but only read and digested fully now when talking to some friends about
Pin
and it had come up. Absolutely worth the read!Pin, Unpin, and why Rust needs them - I’m just a sucker for more articles regarding async topics in Rust. Especially given how much I use it at work these days. It’s a nice little article that gives you a why, not just a how.
Crates
Typing The Technical Interview Rust - I had a hard time deciding where to put this one but went with crate given it’s code. It’s a Rust port of one of Aphyr’s excellent articles that also inspired my most popular article with a Rust flavored version. If you like type level hackery it’s worth reading the source code. It really shows how powerful the type system is and you can do some really wild things with it.