Rust: Tiny little traits
Rust’s trait system has a feature that is often talked about, but which I don’t see used that often in application code: Implementing your traits for types that are not yours. You can see this a lot in the standard library, and also in some libraries (hello itertools
), but I see developers shy away from doing that when writing applications. It’s so much fun and so useful, though!
I’ve started defining and implementing traits for other types a lot more and I have the feeling that my code has become a lot clearer and more intentional. Let’s see what I did.
One-liner traits #
I was tasked to write a DNS resolver that blocks HTTP calls to localhost. Since I build upon hyper
(as you all should), I implemented a Tower service that serves as a middleware. In this middleware, I do the actual check for resolved IP addresses:
let addr = req.as_str();
let addr = (addr, 0).to_socket_addrs();
if let Ok(addresses) = addr {
for a in addresses {
if a.ip().eq(&Ipv4Addr::new(127, 0, 0, 1)) {
return Box::pin(async { Err(io::Error::from(ErrorKind::Other)) });
}
}
}
It’s not bad, but there’s room for potential confusion, and it’s mostly in the conditional:
- We might want to check for more IPs that could resolve to localhost, e.g. the IP
0.0.0.0
.to_socket_addr
might not resolve to0.0.0.0
, but the same piece of code might end up at some other place where this might be troublesome. - Maybe we want to exclude other IPs as well which aren’t localhost. This conditional would be ambiguous.
- We forgot that an IP v6 address exists 🫢
So, while it’s fine, I want to have something where I’m more prepared for things in the future.
I create an IsLocalhost
trait. It defines one function is_localhost
that takes a reference of itself and returns a bool
.
pub(crate) trait IsLocalhost {
fn is_localhost(&self) -> bool;
}
In Rust’s std::net
, there are exactly two structs where you can directly check if the IP addresses are localhost or not. The Ipv4Addr
and Ipv6Addr
structs.
impl IsLocalhost for Ipv4Addr {
fn is_localhost(&self) -> bool {
Ipv4Addr::new(127, 0, 0, 1).eq(self) || Ipv4Addr::new(0, 0, 0, 0).eq(self)
}
}
impl IsLocalhost for Ipv6Addr {
fn is_localhost(&self) -> bool {
Ipv6Addr::new(0, 0, 0, 0, 0, 0, 0, 1).eq(self)
}
}
Checking if an IP is localhost happens exactly where the IP is defined. std::net
has an enum IpAddr
to distinguish between V4, and V6. Let’s implement IsLocalhost
for IpAddr
as well.
impl IsLocalhost for IpAddr {
fn is_localhost(&self) -> bool {
match self {
IpAddr::V4(ref a) => a.is_localhost(),
IpAddr::V6(ref a) => a.is_localhost(),
}
}
}
With the enum, we’re making sure that we don’t forget about V6 IP addresses. Phew. On to SocketAddr
, the original struct we get from to_socket_addr
. Let’s implement IsLocalhost
for that as well.
impl IsLocalhost for SocketAddr {
fn is_localhost(&self) -> bool {
self.ip().is_localhost()
}
}
Great! Turtles all the way down. And it doesn’t matter which struct we’re dealing with. We can check for localhost everywhere.
When calling to_socket_addr
we’re not getting a SocketAddr
directly, but rather an IntoIter<SocketAddr>
, going down the entire route of IP addresses until we reach the actual server. We want to check if any of those is_localhost
, so we see if the collection we get from the iterator has localhost. Another trait!
pub(crate) trait HasLocalhost {
fn has_localhost(&mut self) -> bool;
}
impl HasLocalhost for IntoIter<SocketAddr> {
fn has_localhost(&mut self) -> bool {
self.any(|el| el.is_localhost())
}
}
And that’s it. I like the last implementation a lot because it makes use of iterator methods and closures. In this one-liner, this becomes so wonderfully readable.
Let’s change the original code:
let addr = req.as_str();
let addr = (addr, 0).to_socket_addrs();
if let Ok(true) = addr.map(|mut el| el.has_localhost()) {
return Box::pin(async { Err(io::Error::from(ErrorKind::Other)) });
}
Not that much of a change, but it becomes so obvious what’s happening. It says in the conditional that we’re checking for localhost, and for nothing else. The problem we’re solving becomes clear. Plus, we can do localhost checks at other places as well because the structs give us this information. ❤️
The lazy printer #
I’m using one-liner traits with implementations on other types a lot. This is one utility trait I use a lot when developing. I’m coming from JavaScript, so my most reliable debugger was stdout. I do Debug
prints a lot, but I’m always very clumsy writing println!("{:?}", whatever);
. This calls for a new trait!
trait Print {
fn print(&self);
}
… which I implement for every type that implements Debug
.
impl<T: std::fmt::Debug> Print for T {
fn print(&self) {
println!("{:?}", self);
}
}
Fantastic!
"Hello, world".print();
vec![0, 1, 2, 3, 4].print();
"You get the idea".print()
What a nice utility. Tiny, little traits to make my life easier.