Adapter Pattern in Rust: Overcoming the Orphan Rule with Newtype and Extension Traits

Some time ago, we were talking about the Newtype pattern in the Rust programming language. For that pattern, we used a single-field tuple struct to either alter the inner type semantics or to hide the implementation type from the end user - often we wanted both.
This time, we will also take a closer look into the single-field tuple struct, this time to implement the adapter pattern. Let's start by stating the problem we want to solve with it.
Orphan Rule
I find the Rust typesystem to be one of the most difficult things to get a good grasp on - solely because it is very different from other object-oriented languages we are used to.
One of the unusual things is the relationship between Traits and user-defined types. Traditionally, traits are introduced as the implementation of interfaces. As they serve similar purposes - behavioral level abstraction - I personally am careful introducing traits as interfaces, as they have a significant difference. Typically, we think of interfaces that are bound to the type the moment the type is declared. So when we create the Orc type, we immediately define all interfaces that it implements - Creature, Foe, Mesh. That is not how traits work. Traits and types are completely orthogonal in the Rust language, defined independently. For public types, it is never possible to claim all the traits defined on the type - that set can always extend in dependent crates.
That, however, introduces a problem. Let's imagine that we have two crates: crate1 and crate2, both single lib.rs files:
// crate1/lib.rs
trait Applicator {
fn apply(&self, a: u32) -> u32;
}
// crate2/lib.rs
struct Multiplicator {
pub scale: u32,
}The creator of crate2 has no clue of crate1's existence, nor the other way around. However there is a crate3 which author wants to use both crates, but needs implementation of Applicator on the Multiplicator type:
use crate1::Applicator;
use crate2::Multiplicator;
impl Applicator for Multiplicator {
fn apply(&self, a: u32) -> u32 {
self.scale * a
}
}This seems reasonable, and it's something we'd genuinely want to do. Bad news is: that code doesn't compile. Let's think of - why is it a problem?
Well, we will now introduce a crate4 that has a bit of a different understanding of how Applicator should be implemented on Multiplicator:
use crate1::Applicator;
use crate2::Multiplicator;
impl Applicator for Multiplicator {
fn apply(&self, a: u32) -> u32 {
a.pow(self.scale)
}
}Well - no problem, right? crate3 uses its own implementation of a trait, crate4 has its own, it is fine. However, things get tricky when we introduce crate5 that depends on all the mentioned crates so far:
use crate1::Applicator;
fn main() {
let m = crate2::Multiplicator { scale: 3 };
println!("{}", m.apply(3));
}Now, there is a question: what does that crate5 print? 9 or 27? It's impossible to decide which implementation is correct here!
The answer could be - just fail to compile when that case occurs! crate5 could simply cause a compilation error due to the conflicting implementation of Applicator on Multiplicator. That would, however, be a short-sighted approach. Maybe when crate5 was created, the crate4 didn't yet need implementation of Applicator and the intention was to use the crate3 implementation that was always there. In such cases, the code would correctly compile up until crate4 added the new trait implementation. That is not something we want - that would make dependency management very difficult.
Instead of that, the Rust creators decided what is called the Orphan Rule. Its full statement is fairly complicated as it involves some cases with generics, but the simple wording I like to use for it is:
To define the trait on a type, the crate you put the impl block in has to own either the trait or the type.
The complicated part is what happens when the type is generic - it turns out that in such cases, the generic type and the trait can come from an external crate, as long as the impl-block crate owns a type used to specialize the generic. The full rule can be found in the Rust reference.
The problem
Now let's go to the crate3 from the example above. Let's bring back the code:
use crate1::Applicator;
use crate2::Multiplicator;
impl Applicator for Multiplicator {
fn apply(&self, a: u32) -> u32 {
self.scale * a
}
}Here is what happens when we try to compile it:
error[E0117]: only traits defined in the current crate can be implemented for types defined outside of the crate
--> src/lib.rs:4:1
|
4 | impl Applicator for Multiplicator {
| ^^^^^^^^^^^^^^^^^^^^-------------
| |
| `Multiplicator` is not defined in the current crate
|
= note: impl doesn't have any local type before any uncovered type parameters
= note: for more information see https://doc.rust-lang.org/reference/items/implementations.html#orphan-rules
= note: define and implement a trait or new type insteadHowever, this use case is very often what we really need! One place where I faced this problem recently is using the axum crate for web server. It has its axum::response::IntoResponse trait that should be implemented on types that can be converted to an HTTP response. A nice property of this trait is that if the axum::response::IntoResponse is implemented on both T and E types, then it is also implemented on Result<T, E> - so we simply want to implement the trait on our error types we define!
As this is a binary application I am building, not a library, I like to use eyre (or anyhow) crate for my error handling. There is a problem - axum doesn't care about either of those crates. Also those crates have no implementation of axum traits. So if I want my eyre errors to simply generate 500 HTTP responses, I have a problem - I cannot just implement a trait! The simple solution here is the Adapter pattern.
Adapter pattern
The adapter pattern is a simple newtype, just like the compiler suggested, implementing traits that are missing from our external type. Let's fix our crate3 with it:
struct ApplicatorAdapter(crate2::Multiplicator);
impl crate1::Applicator for ApplicatorAdapter {
fn apply(&self, a: u32) -> u32 {
self.0.scale * a
}
}
impl ApplicatorAdapter {
fn new(src: crate2::Multiplicator) -> Self {
Self(src)
}
}Now, to use the Applicator trait, we would simply construct the adapter first:
fn mul(a: u32, b: u32) -> u32 {
let m = Multiplicator { scale: a };
let adapter = ApplicatorAdapter::new(m);
adapter.apply(b)
}Reference as underlying type
The solution above is already working. However, it comes with a huge downside: to construct the adapter, one needs full ownership over the Multiplicator. That makes the adapter completely unusable if we either want to keep the original object later, or we have only access to the reference, which, based on Applicator definition, should be sufficient.
To solve it, we often make the adapter holding the reference instead of the owned type under the hood. With this approach, the crate3 could look like this:
struct ApplicatorAdapter<'a>(&'a crate2::Multiplicator);
impl crate1::Applicator for ApplicatorAdapter<'_> {
fn apply(&self, a: u32) -> u32 {
self.0.scale * a
}
}
impl<'a> ApplicatorAdapter<'a> {
fn new(src: &'a crate2::Multiplicator) -> Self {
Self(src)
}
}To use the reference as an inner type, we had to introduce the lifetime generic as every single reference needs to be bound to some lifetime. In this case, it prevents the ApplicatorAdapter struct from ever outliving the Multiplicator it was created from. This makes sense; the wrapper holds reference, so if it outlived the inner Multiplicator we would have a very dangerous dangling reference.
Note that if the Applicator trait would require &mut self on its method, we would simply use the mutable reference under the hood. That would, however, be a bit problematic - that requires to always have access to the mutable reference to the object even for calling &self methods of Applicator. In such cases, the solution is typically to go with an owning adapter, and provide an API to deconstruct it back to the inner object like fn into_inner(self) -> Multiplicator method on the adapter type.
Extension trait for creation
While the adapter pattern solved the problem of getting around the orphan rule, it introduced the new one: the API is now annoying. We need to construct the entire new type for a single call, the name is long, and a lot of characters to type and read. One of the solutions could be to shorten the adapter type name, but I am not a fan of the final API anyway.
The common solution to solve it is yet another pattern very common for Rust - the extension trait. The idea is to introduce a new crate-private trait that we will implement on the adapted type to add new functionality to it. Orphan rule won't stop us - we own the trait definition. Here is how it could work:
trait AdapterBuilder {
fn applicator(&self) -> impl crate1::Applicator;
}
impl AdapterBuilder for Multiplicator {
fn applicator(&self) -> impl crate1::Applicator {
ApplicatorAdapter(self)
}
}
fn mul(a: u32, b: u32) -> u32 {
let m = Multiplicator { scale: a };
m.applicator().apply(b)
}We can also use the Applicator when we have access to a Multiplicator reference:
fn mul1(m: Multiplicator, b: u32) -> u32 {
m.applicator().apply(b)
}Deref and AsRef on the adapter type
Now I would like to mention a slightly controversial topic - should we implement Deref and maybe DerefMut for the adapter types?
The first thing to bring here is, for sure, the Deref documentation:
Warning: Deref coercion is a powerful language feature which has far-reaching implications for every type that implements Deref. The compiler will silently insert calls to Deref::deref. For this reason, one should be careful about implementing Deref and only do so when deref coercion is desirable. See below for advice on when this is typically desirable or undesirable.
Types that implement Deref or DerefMut are often called “smart pointers” and the mechanism of deref coercion has been specifically designed to facilitate the pointer-like behavior that name suggests. Often, the purpose of a “smart pointer” type is to change the ownership semantics of a contained value (for example, Rc or Cow) or the storage semantics of a contained value (for example, Box).
The big question is if coercion is desirable in the adapter case. The typical example mentioned by the documentation is smart pointers. It's pretty difficult to call the adapter the smart pointer - it has nothing to do with the storage semantics. Still, the question is whether the documentation strictly denies usage of Deref on non-smart pointers?
I personally implement Deref and DerefMut traits on my adapter types very often, especially if those are owning adapters. It is always dependent on the fact how I plan to use the adapter. If it is a short-living ad hoc type that I create in a scope just to call some trait methods, I might skip Deref. On the other hand I would often create a single adapter for type that is very fundamental for my solution that would implement a whole set of missing traits, and I would simply use that type almost everywhere instead of the adapted one - I might then define both Deref and DerefMut to keep the original type implementation through coercion - which is very often what I actually want.
However, if implementing Deref is too much idiom-violating for you, the alternative could be to implement AsRef and maybe AsMut traits to maintain access to the original object on demand. The problem with those traits is that they are generic and there are cases where explicit type specification pointing to which type we want to explicitly coerce might be required.
Yet another solution might be to provide function for accessing inner type directly on the adapter type:
impl<'a> ApplicatorAdapter<'a> {
fn as_inner(&self) -> &'a Multiplicator {
self.0
}
}The typical method to access mutable reference (for mutable reference or owned adapters) would be as_inner_mut.
Solving Axum problem with adapter
Let's now come back to the practical problem I faced – bridging the axum with the eyre crate. Axum provides the axum::response::IntoResponse trait that we would really like to keep implemented on error types we use in our solution - in my case it was eyre::Report. However, orphan rule prevents us from implementing axum traits on eyre type. We can fix it using the adapter pattern:
struct ErrorAdapter(eyre::Report);
impl axum::response::IntoResponse for ErrorAdapter {
fn into_response(self) -> axum::response::Response {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error",
).into_response()
}
}To make this ergonomic at the call site, we can implement From so that the ? operator works directly inside handlers:
impl From<eyre::Report> for ErrorAdapter {
fn from(e: eyre::Report) -> Self {
ErrorAdapter(e)
}
}
async fn my_handler() -> Result<impl IntoResponse, ErrorAdapter> {
let data = fetch_data().await?; // eyre::Report converts into ErrorAdapter via From
Ok(axum::Json(data))
}Multiple adapters on trait-type pair
Our solution to the axum-eyre bridging error has one problem. All errors we return ending up as 500. It is probably the best default, still there are cases where we want to add a more specialized version of an error. We can do it by providing multiple adapters for our eyre error type:
struct ErrorAdapter404(eyre::Report);
impl axum::response::IntoResponse for ErrorAdapter404 {
fn into_response(self) -> axum::response::Response {
(
axum::http::StatusCode::NOT_FOUND,
"Resource not found",
).into_response()
}
}
struct ErrorAdapter403(eyre::Report);
impl axum::response::IntoResponse for ErrorAdapter403 {
fn into_response(self) -> axum::response::Response {
(
axum::http::StatusCode::FORBIDDEN,
"Forbidden resource",
).into_response()
}
}
trait ErrExt {
fn err500(self) -> ErrorAdapter;
fn err404(self) -> ErrorAdapter404;
fn err403(self) -> ErrorAdapter403;
}
impl ErrExt for eyre::Report {
fn err500(self) -> ErrorAdapter { self.into() }
fn err404(self) -> ErrorAdapter404 { ErrorAdapter404(self) }
fn err403(self) -> ErrorAdapter403 { ErrorAdapter403(self) }
}Note that we can now just use straight ? to use the Into error conversion, or we can use my_result.map_err(ErrExt::err404) to wrap inner error with the 404 adapter. However with extension traits we can go one step further - we can extend the result to make API even better!
trait ResultExt {
type Ok;
fn or500(self) -> Result<Self::Ok, ErrorAdapter>;
fn or404(self) -> Result<Self::Ok, ErrorAdapter404>;
fn or403(self) -> Result<Self::Ok, ErrorAdapter403>;
}Now we can still use ? when we want to report 500 errors, but we can also simply call my_result.or404()? to convert the error to the adapter specialized in returning 404 errors.
We could possibly find an even more robust solution - the error code could maybe be injected to the specialized adapter to save us having separate types for every error code. Another possibility would be to use the error code traits as marker types for ErrorAdapter, providing only the error code as a trait-associated constant. Those, however, are further extensions of the same adapter idea, and for the sake of consistency, I would only leave them as improvement ideas without digging into them further.
Adapter alternatives - trait crate utilities
It's worth noting that the adapter pattern is not always needed to overcome the Orphan Rule’s limitations. There are sometimes ways to overcome the problem. A good example will be the serde crate. If we are using a type from the external crate that does not implement the Serialize and Deserialize serde traits, it might be tempting to create an adapter type implementing them. However, it's not necessary. Serde authors noticed that this is a common case and provided the special remote syntax to solve this problem for us.
In the initial example we considered previously, there was the Multiplicator type not implementing the serde types. Serde provides us with a specialized way to delegate serialization of such types without manually implementing Serialize or Deserialize which is often complex:
#[derive(Serialize, Deserialize)]
#[serde(remote = "crate2::Multiplicator")]
struct MultiplicatorDef {
pub scale: u32,
}
#[derive(Serialize, Deserialize)]
struct Wrapper {
#[serde(with = "MultiplicatorDef")]
multiplicator: Multiplicator,
}The remote syntax is itself very serde-specific and it has applications beyond implementing serde traits of other crate types, but that is beyond the article scope. My point here is to mention that before we look towards an adapter pattern to overcome the orphan rule limitation, it would be a good habit to check if the crate defining the trait has some special syntax helping us in a more consistent way.
Public or private?
There is one thing I didn't cover yet, and that was a point of discussion about newtype patterns. Should the only field of the type be public or private?
In contrast to the pure newtype pattern hiding the inner implementation type is not our concern we are solving. That being said, there is often no real reason to keep the field private. There is, however, another question: is .0 a good API to be used around? I personally don't like it, and if I used the single-field tuple struct for my adapters, I would often keep them private and instead provide access to the field either via Deref trait or the specialized method.
On the other hand, if we are introducing an extra method, maybe a better approach could be to make the single field the named field instead of using tuple struct. The ApplicatorAdapter could be reshaped to:
struct ApplicatorAdapter {
pub inner: Multiplicator,
}As long as there are no additional reasons to hide the underlying type, I find this approach idiomatic. This is often a matter of taste, and the extra circumstances of how the type is used. So my take is: in the context of the adapter itself the visibility is not a concern.
Summary
Today, we took a closer look at another application of the newtype-like pattern: the adapter. It is particularly useful in Rust - more so than in other languages - due to the orthogonal relationship between traits and types, and the Orphan Rule that follows from it.
The key takeaway is that the adapter is a general solution. Serde has remote, some crates offer feature flags for cross-crate trait implementations, but when none of that exists - and it often doesn't - the adapter is the great tool. We saw this with axum and eyre: two widely used crates that have no knowledge of each other, with no built-in bridge between them.
We also explored how to make adapters ergonomic. A raw adapter type on its own can feel clunky, but extension traits, From implementations, and careful API design can make them nearly invisible at the call site. The pattern scales naturally from a single trait-type pair to multiple specialized adapters, as we saw with the HTTP error code variants.
In the next article, we will take a closer look at extension traits - a pattern we already used here to improve the adapter ergonomics, but one that deserves its own spotlight as a powerful Rust idiom.
