Edit: In the original post I said that the PartialEq trait bound was required in the generic Display function implementation; however, if the last element of the array is the same as its predecessor then the function will not print properly. Thanks to korrat for letting me know and for the recommendation to use std::ptr::eq instead.
Probably one of the first things that someone does when learning a new programming language is to implement the famous "Hello, world!"
message, which in the case of Rust comes with the boilerplate code via the cargo new
command:
fn main() {
println!("Hello, world!");
}
And then, start experimenting on how to display different types
in the terminal, like strings
, integers
, floats
, etc.:
fn main() {
let planet = "Earth";
let surface_area = 510072000;
let polar_radius = 6356.752;
println!(
"Hello, {}!\nSurface area: {}\nPolar radius: {}",
planet, surface_area, polar_radius
);
}
λ cargo run -q
Hello, Earth!
Surface area: 510072000
Polar radius: 6356.752
In Rust it's pretty straightforward until we try to print a struct
or other user-defined types:
struct Planet {
name: String,
surface_area: i64,
polar_radius: f64,
}
fn main() {
let planet = Planet {
name: "Earth".to_string(),
surface_area: 510072000,
polar_radius: 6356.752,
};
println!("{}", planet);
}
The compiler yields an error and advice:
λ cargo run -q
error[E0277]: `Planet` doesn't implement `std::fmt::Display`
--> src/main.rs:20:20
|
20 | println!("{}", planet);
| ^^^^^^ `Planet` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `Planet`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `temp` due to previous error
What's happening here?
The Debug and Display traits
The compiler is basically saying that it doesn't know how to print the struct Planet
type.
However, it's providing two alternatives:
- Use the built-in formatters with
{:?}
or{:#?}
in theprintln!
macro, which requires implementing theDebug
trait on thestruct Planet
type, or - Implement the
std::fmt::Display
trait and define the formatter for the type.
Let's review both alternatives in more detail.
First, the Debug
trait can be implemented using the #[derive(Debug)]
attribute:
#[derive(Debug)]
struct Planet {
name: String,
surface_area: i64,
polar_radius: f64,
}
fn main() {
let planet = Planet {
name: "Earth".to_string(),
surface_area: 510072000,
polar_radius: 6356.752,
};
println!("{:?}", planet);
println!("{:#?}", planet);
}
And when running the program, the struct Planet
type is printed using the built-in formatters provided by the Debug
trait:
λ cargo run -q
Planet { name: "Earth", surface_area: 510072000, polar_radius: 6356.752 }
Planet {
name: "Earth",
surface_area: 510072000,
polar_radius: 6356.752,
}
On the other hand, implementing the Display
trait isn't as straightforward as Debug
, but can be accomplished by bringing std::fmt::Display
into scope and writing an impl
block for the struct Planet
type, which defines the custom formatter in the fn fmt(...)
function:
use std::fmt::Display;
struct Planet {
name: String,
surface_area: i64,
polar_radius: f64,
}
impl Display for Planet {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"-> {}:\n\tSurface: {} km2\n\tPolar radius: {} km",
self.name, self.surface_area, self.polar_radius
)
}
}
fn main() {
let planet = Planet {
name: "Earth".to_string(),
surface_area: 510072000,
polar_radius: 6356.752,
};
println!("{}", planet);
}
Implementing Display
allows the definition of a custom printing format for the type:
λ cargo run -q
-> Earth:
Surface: 510072000 km2
Polar radius: 6356.752 km
Arrays and the 'Orphan Rule'
Now, let's try to follow the same steps but with an Array
…
First, by defining an array of four f64
elements and trying to print it with println!
:
type MyArray = [f64; 4];
fn main() {
let my_array: MyArray = [1.5, 2.5, 3.5, 4.5];
println!("{}", my_array);
}
The compiler yields the same error code and advice for the Array
type, just as it did for the struct
type:
λ cargo run -q
error[E0277]: `[f64; 4]` doesn't implement `std::fmt::Display`
--> src/main.rs:25:20
|
25 | println!("{}", my_array);
| ^^^^^^^^ `[f64; 4]` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `[f64; 4]`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
Following the compiler's guidance and using the built-in "{:?}"
and "{:#?}"
formatters works, just like with the struct
type, and the array is printed:
λ cargo run -q
[1.5, 2.5, 3.5, 4.5]
[
1.5,
2.5,
3.5,
4.5,
]
Great!
Now, let's try to define a custom formatter via the Display
trait for the Array
type, just as we did with the struct
:
type MyArray = [f64; 4];
impl Display for MyArray {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}, {}, {}, {}", self[0], self[1], self[2], self[3])
}
}
fn main() {
let my_array: MyArray = [1.5, 2.5, 3.5, 4.5];
println!("{}", my_array);
}
λ cargo run -q
error[E0117]: only traits defined in the current crate can be implemented for arbitrary types
--> src/main.rs:25:1
|
25 | impl Display for MyArray {
| ^^^^^^^^^^^^^^^^^-------
| | |
| | this is not defined in the current crate because arrays are always foreign
| impl doesn't use only types from inside the current crate
|
= note: define and implement a trait or new type instead
For more information about this error, try `rustc --explain E0117`.
Ups! Unfortunately, this doesn't work like it did with the struct
type.
Looking closely at the compiler feedback, it's complaining that arrays are always foreign types.
If we run rustc --explain E0117
the compiler provides additional details about what is happening here:
This error indicates a violation of one of Rust's orphan rules for trait
implementations. The rule prohibits any implementation of a foreign trait (a
trait defined in another crate) where
- the type that is implementing the trait is foreign
- all of the parameters being passed to the trait (if there are any) are also
foreign.
To avoid this kind of error, ensure that at least one local type is referenced
by the `impl`...
It turns out that by implementing Display
on any Array
we're violating Rust's Orphan Rule which essentially forbid implementing a foreign trait on a foreign type.
A foreign type or trait is that which isn't local to our crate.
A local type or trait is that which is defined in our crate.
So, to overcome the Orphan Rule we must either:
- Implement a local trait on a foreign type:
impl MyCustomTrait for Vec<T>
, or - Implement a foreign trait on a local type:
impl Display for MyStruct
.
Because the struct Planet
was a type defined in our crate we were able to implement Display
, a foreign trait, for it.
However, even if we define an Array
in our crate, like we did with MyArray
above, Rust will always treat arrays as a foreign type.
Display
will always be a foreign trait and any Array
that we define locally in our crate will always be a foreign type…
What can be done?
The Newtype Pattern to the rescue
According to the Rust Programming Language book, the Newtype pattern
comes from the Haskell Programming Language Newtype, and the idea is to essentially define a local wrapper type that encloses the foreign type
in order to implement the foreign trait
for the wrapper.
So, in a summary:
- Define a new
Wrapper
type local to the crate. - Include the
foreign type
as an element of theWrapper
type. - Implement the
foreign trait
for theWrapper
type.
One convenient approach is to use a thin wrapper, that's a Wrapper type
that provides a straightforward way to get to the enclosed or wrapped type
with minimum friction.
A Tuple Struct
is a suitable thin wrapper type:
type MyArray = [f64; 4];
struct Wrap(MyArray);
It's not verbose, not redundant and allows a straightforward access to the underlying type with wrap.0
:
fn main() {
let my_array: MyArray = [1.5, 2.5, 3.5, 4.5];
let my_wrap = Wrap(my_array);
println!("{:?}", my_wrap.0)
}
λ cargo run -q
[1.5, 2.5, 3.5, 4.5]
Now it's possible to implement the foreign Display
trait for the local Wrap
type that encloses the foreign Array
type:
type MyArray = [f64; 4];
struct Wrap(MyArray);
impl Display for Wrap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"-> Array:\n{}, {}, {}, {}",
self.0[0], self.0[1], self.0[2], self.0[3]
)
}
}
fn main() {
let my_array: MyArray = [1.5, 2.5, 3.5, 4.5];
let my_wrap = Wrap(my_array);
println!("{}", my_wrap)
}
When the program is executed, the custom format for the Array
type is used via the Display
trait:
λ cargo run -q
-> Array:
1.5, 2.5, 3.5, 4.5
This code works well but is rather limited to:
- A single data type:
f64
. - An array of size
4
. - One
Array
type.
It's possible to leverage Rust's generics to make it more extendable and idiomatic.
Extensibility with Generics
The first thing to do is make Array
a generic type:
type MyArray<T, N> = [T; N];
However, the code above will fail to compile because 'N', which represents the size of the array, isn't a value:
error[E0423]: expected value, found type parameter `N`
--> src/main.rs:22:26
|
22 | type MyArray<T, N> = [T; N];
| ^ not a value
It's necessary to use Rust's Const Generics in order to make the array completely generic, as per the RFC definition:
Const Generics allow types to be generic over constant values; among other things this will allow users to write impls which are abstract over all array types.
type MyArray<T, const N: usize> = [T; N];
And now, it's possible to make the Wrap
type generic as well:
struct Wrap<T, const N: usize>(MyArray<T, N>);
Next, we need to make the impl
of Display
generic for arrays of any type and of any size, by declaring the impl
with the same generics as those of the Wrap:
impl<T, const N: usize> Display for Wrap<T, N>
Then it's necessary to bound the Array
type to those types that implement the Display
trait, plus any other trait that's required for our particular implementation:
impl<T, const N: usize> Display for Wrap<T, N>
where
T: Display,
In this particular case, the bound with the (As pointed by korrat, it's better to use PartialEq
trait is required because I'm making an equality comparison between elements of the array.std::ptr::eq
as the comparison function between the references in order to avoid the incorrect printing if the last element of the array is the same as its predecessor.)
Last thing to do is to implement the required Display
trait function: fn fmt(...) -> std::fmt::Result
, with the desired custom formatter:
use core::fmt::Display;
use std::ptr::eq;
struct Wrap<T, const N: usize>([T; N]);
impl<T, const N: usize> Display for Wrap<T, N>
where
T: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let separator = ", ";
let mut s = "Array -> ".to_string();
for element in &self.0 {
s.push_str(&element.to_string());
if !eq(element, self.0.last().unwrap()) {
s.push_str(&separator);
}
}
write!(f, "{}", s)
}
}
fn main() {
let my_array = [1, 3, 1];
let my_wrap = Wrap(my_array);
println!("{}", my_wrap);
}
Finally!
It's now possible to define an Array
of any size and of any type, so long as that type implements Display
, and print it using the println!
macro with a custom formatter:
fn main() {
let my_array_f = [1.5, 2.5, 3.5, 4.5, 5.2, 6.75, 8.90, 8.90, 8.90];
let my_array_c = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'j', 'j'];
let my_array_s = [
"Hello".to_string(),
"World!".to_string(),
"from Rust!".to_string(),
];
let my_wrap = Wrap(my_array_f);
println!("{}", my_wrap);
let my_wrap = Wrap(my_array_c);
println!("{}", my_wrap);
let my_wrap = Wrap(my_array_s);
println!("{}", my_wrap);
}
Rust's powerful type inference detects the type of the Array
and it's size, thanks to const generics!
And the result in the terminal is the following:
λ cargo run -q
Array -> 1.5, 2.5, 3.5, 4.5, 5.2, 6.75, 8.9, 8.9, 8.9
Array -> a, b, c, d, e, f, g, h, i, j, j, j
Array -> Hello, World!, from Rust!
Links, references and disclaimers:
- Header Photo by Faris Mohammed on Unsplash.
- The Rust Programming Language.
- The Rust Programming Language book.
- Rust's Const Generics RFC: 2000-const-generics.
- The Haskell Programming Language.
- The Haskell Programming Language Newtype.
Top comments (0)