I’m trying to understand Rust traits and an exercise I had in mind was to implement a serializer.
The idea would be the following: it outputs things in the form <type=TYPE,value=VALUE>
where TYPE
and VALUE
are placeholders for types and values.
The serializer would support a string type string
and also an array type array
. The array can contain any valid type, include the array
type, in other words it can be nested.
These would be valid outputs:
- for the string
hello world
:<type=string,value=hello world>
, - for the array containing
foo
andbar
:<type=array,value=[<type=string,value=foo>,<type=string,value=bar>]>
, - for the array containing the array containing
foo
and thenbar
(in JSON it would be[["foo"], "bar"]
) :<type=array,value=[<type=array,value=[<type=string,value=foo>]>,<type=string,value=bar>]>
, - etc.
My idea was to provide a single function serialize
that would take:
- something we can write into (for example a
std::io::Write
), - a value implementing a
Serializable
trait.
When called, serialize
would apply the Serializable
implementation of the provided type:
pub mod serializer {
pub fn serialize<W: std::io::Write, V: Serializable>(
writer: &mut W,
value: V,
) -> std::io::Result<()> {
value.serialize(writer)
}
pub trait Serializable {
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()>;
}
}
Now, to support the string type, we can implement Serializable
for &str
:
pub mod serializer {
pub fn serialize<W: std::io::Write, V: Serializable>(
writer: &mut W,
value: V,
) -> std::io::Result<()> {
value.serialize(writer)
}
pub trait Serializable {
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()>;
}
impl Serializable for &str {
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
write!(writer, "<type=string,value={}>", self)
}
}
#[cfg(test)]
mod test {
use super::serialize;
#[test]
fn test_serialize_str() {
let mut buffer = Vec::new();
serialize(&mut buffer, "hello world").unwrap();
assert_eq!(
String::from_utf8(buffer).unwrap(),
"<type=string,value=hello world>"
);
}
}
}
To implement the array serialization, I started by implementing Serializable
for the Vec
. However this approach works only when all values of the Vec
are of the same type.
...
impl<T: Serializable + Clone> Serializable for Vec<T> {
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
write!(writer, "<type=array,value=[")?;
let mut sep = "";
for v in self {
write!(writer, "{}", sep)?;
serialize(writer, v.clone())?;
sep = ","
}
write!(writer, "]>")
}
}
...
#[test]
fn test_serialize_vec_of_str() {
let mut buffer = Vec::new();
serialize(&mut buffer, vec!["hello", "world"]).unwrap();
assert_eq!(
String::from_utf8(buffer).unwrap(),
"<type=array,value=[<type=string,value=hello world>,<type=string,value=hello world>]>"
);
}
To support arrays of any other type, I naively tempted to implement Serializable
for Vec<Box<dyn Serializable>>
. I understand Vec<Box<dyn Serializable>>
as a Vec
of anything that implements the Serializable
trait.
impl Serializable for Vec<Box<dyn Serializable>> {
fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
write!(writer, "<type=array,value=[")?;
...
write!(writer, "]>")
}
}
However rust does not compile it, and yields the following errors.
What prevents Serializable
to be implemented for Vec<Box<dyn Serializable>>
?
The compiler then suggests to use an enum and dispatch according to it but I’m not sure to understand how it would help, is it linked or a different issue?
Thanks!
Compiling playground v0.0.1 (/playground)
error[E0038]: the trait `Serializable` cannot be made into an object
--> src/lib.rs:19:27
|
19 | impl Serializable for Vec<Box<dyn Serializable>> {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ `Serializable` cannot be made into an object
|
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
--> src/lib.rs:10:12
|
9 | pub trait Serializable {
| ------------ this trait cannot be made into an object...
10 | fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()>;
| ^^^^^^^^^ ...because method `serialize` has generic type parameters
= help: consider moving `serialize` to another trait
= help: the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing `Serializable` for this new enum and using it instead:
&str
std::vec::Vec<std::boxed::Box<(dyn serializer::Serializable + 'static)>>
= note: `Serializable` can be implemented in other crates; if you want to support your users passing their own types here, you can't refer to a specific type
error[E0038]: the trait `Serializable` cannot be made into an object
--> src/lib.rs:20:42
|
20 | fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
| ^^^^ `Serializable` cannot be made into an object
|
note: for a trait to be "object safe" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
--> src/lib.rs:10:12
|
9 | pub trait Serializable {
| ------------ this trait cannot be made into an object...
10 | fn serialize<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()>;
| ^^^^^^^^^ ...because method `serialize` has generic type parameters
= help: consider moving `serialize` to another trait
= help: the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing `Serializable` for this new enum and using it instead:
&str
std::vec::Vec<std::boxed::Box<(dyn serializer::Serializable + 'static)>>
= note: `Serializable` can be implemented in other crates; if you want to support your users passing their own types here, you can't refer to a specific type