Join GitHub today
GitHub is home to over 31 million developers working together to host and review code, manage projects, and build software together.
Sign upHigher order serialization #552
Comments
This comment has been minimized.
This comment has been minimized.
|
I'm not sure I understand. Could you show an example where you can't express it in serde without intermediate serializeables? |
This comment has been minimized.
This comment has been minimized.
|
Sure
trait ObjectSafeSerialize<S: Serializer> {
fn serialize(&self, serializer: &mut S) -> Result<(), S::Error>;
}That is, using this definition, if you know what You can implement this trait for all impl<T, S> ObjectSafeSerialize<S> for T where T: Serialize {
fn serialize(&self, serializer: &mut S) -> Result<(), S::Error> {
<Self as Serialize>::serialize(self, serializer)
}
}You can also include methods which forward serialization in specific positions; that is, you could add this method to that trait and impl and it will work (because fn serialize_seq_elt(&self, serializer: &mut S, state: &mut S::SeqState) -> Result<(), S::Error> {
serializer.serialize_seq_elt(state, self)
}So I now have the ability to serialize heterogenous types if I have the serializer present when those trait objects are constructed. I have this type, and I want to serialize it the same way the previous type was serialized: struct Foo {
tag: u32
}
impl Foo {
fn members<S: Serializer>(&self) -> Vec<ObjectSafeSerialize<S>> { ... }
}The problem is that I cannot define However, if I had these higher order functions, I could do this: impl Serialize for Foo {
fn serialize<S: Serializer>(&self, serializer: &mut S) -> Result<(), S::Error> {
let mut state = try!(serializer.serialize_map());
try!(serializer.serialize_map_key(&mut state, "tag"));
try!(serializer.serialize_map_value(&mut state, &self.tag));
try!(serializer.serialize_map_key(&mut state, "members"));
try!(serializer.serialize_map_value_of(&mut state, |serializer| {
let mut state = try!(serializer.serialize_seq());
for member in self.members::<S>() {
try!(member.serialize_seq_elt(serializer, &mut state));
}
serializer.serialize_seq_end(state)
}));
serializer.serialize_map_end(state)
}
} |
This comment has been minimized.
This comment has been minimized.
|
However, beyond this example, I think these functions would provide a meaningful win in ergonomics for users trying to define serialization to some complex schema. |
This comment has been minimized.
This comment has been minimized.
|
Oh gosh, I just reread your comment and I realized I misunderstood. I thought you meant something that can't be be expressed even with intermediate structs, but you wanted something where intermediate structs are necessary. The example above is something that can't be expressed in serde at all right now. The more general case that this improves the ergonomics of is this: I have a schema like this: { "bar": { ... }, "baz": { ... } }But internally, the feilds of both are in one struct. struct Foo {
// bar fields,
// baz fields
}
impl Serialize for Foo {
fn serialize<S: Serializer>(&self, serializer: &mut S) -> Result<(), S::Error> {
let bar = Bar { /* bar fields of self */ };
let baz = Baz { /* baz fields of self */ };
//serialize a map with both bar and baz.
}
}
struct Bar<'a> {
// references to bar fields
}
impl<'a> Serialize for Bar<'a> { ... }
struct Baz<'a> {
// references to baz fields
}
impl<'a> Serialize for Baz<'a> { ... }With these methods, you could drop these temporaries entirely and just provide this multi-level structure in the serialize for Foo. Basically, the issue is when you have to meet a schema that isn't consistent with your internal representation of the data. |
This comment has been minimized.
This comment has been minimized.
|
I wonder if we could simply implement |
This comment has been minimized.
This comment has been minimized.
|
Yes, I think that would work if there aren't any coherence problems. |
This comment has been minimized.
This comment has been minimized.
|
@withoutboats: hello! Yes, as of right now serde isn't object safe because we found a lot of value with being able to switch serializers and deserializers during the process. I originally did have closures but switched towards this visitor pattern way back when our closures had an implicit allocation. I've been meaning on exploring how they could help with modern design. However, one limitation with this approach is how would you handle the whole deserialization problem? Right now serde_codegen generates a bit of an obnoxious state machine to pull apart fields since they might come back in arbitrary order. You'd end up having to do this yourself to avoid these intermediate structures. Speaking for your specific example though, this will work with serde today since there's already an implementation for slices (and assuming someone's already implemented the right impls for the struct Foo {
tag: u32,
members: Vec<Bar>,
}
impl Serialize for Foo {
fn serialize<S: Serializer>(&self, serializer: &mut S) -> Result<(), S::Error> {
let mut state = try!(serializer.serialize_map());
try!(serializer.serialize_map_key(&mut state, "tag"));
try!(serializer.serialize_map_value(&mut state, &self.tag));
try!(serializer.serialize_map_key(&mut state, "members"));
try!(serializer.serialize_map_value(&mut state, &self.members));
serializer.serialize_map_end(state)
}
}Furthermore, with serde_codegen, you can really just do: #[derive(Serialize)]
struct Foo {
tag: u32,
members: Vec<Bar>,
}And serde will generate the exact same code that I wrote above. @oli-obk: that's an interesting idea! I haven't thought about doing crazy magic like that. |
This comment has been minimized.
This comment has been minimized.
|
@erickt hey :-) The original example was a pretty bad explanation of the problem, because you're right - the current implementation works fine for that example. The problem right now is twofold:
My real use case is an implementation of JSON API. Here is the code in question. Because the But if I could have this higher order representation, I could define it this way instead: trait Wrapper<T: api::Resource> {
fn include<S: Serializer>(&self) -> Vec<Box<ObjectSafeSerialize<S>>>;
...
}
struct ResourceDocument<T: api::Resource> {
resource: Resource<T>,
}
impl<T> Serialize for ResourceDocument<T> where T: api::Resource, Resource<T>: Wrapper<T> {
fn serialize<S: Serializer>(&self, serializer: &mut S) -> Result<(), S::Error> {
let included = self.resource.included::<S>();
if included.is_empty() {
...
} else {
let mut state = try!(serializer.serialize_map(Some(3)));
...
try!(serializer.serialize_map_key(&mut state, "included"));
try!(serializer.serialize_map_value(&mut state, &|serializer| {
let mut state = try!(serializer.serialize_seq(Some(included.len());
for resource in included {
try!(resource.serialize_seq_elt(serializer, &mut state));
}
serializer.serialize_seq_end(state)
}));
...
serializer.serialize_map_end(state)
}
}
}The current implementation has to construct multiple BTreeMaps, Strings, and so on for each record, whereas this solution would just have a single virtual call and then serialize them directly to the final representation (probably just writing JSON into the buffer for the HttpResponse body). |
This comment has been minimized.
This comment has been minimized.
|
We are going to need the impl<S, F> Serialize for F
where S: Serializer,
F: Fn(&mut S),
{
#[inline]
fn serialize<S>(&self, serializer: &mut S) -> Result<(), S::Error>
where S: Serializer,
{
self(serializer)
}
} |
This comment has been minimized.
This comment has been minimized.
|
Iiinteresting. The trade off in the What we really want here are higher rank type parameters in trait bounds. trait SerializeTo<S: Serializer> {
fn serialize_to(&self, serializer: &mut S) -> Result<(), S::Error>;
}
impl<T> Serialize for T where T: for<X: Serializer> SerializeTo<X> {
fn serialize<S: Serializer>(&self, serializer: &mut S) -> Result<(), S::Error> {
self.serialize_to(serializer)
}
}There are other configurations of this that might be more sensible also; the point is that higher rank type parameters have the advantage of allowing us to essentially arbitrary move the type parameter up and down scopes. But barring that feature, I don't know what serde should do. Expanding the API surface is definitely eugh, so I understand if you don't want to support my rather specific use case. |
This comment has been minimized.
This comment has been minimized.
|
Well. We can simply |
This comment has been minimized.
This comment has been minimized.
|
Scratch that, |
dtolnay
added
the
discussion
label
Sep 23, 2016
This comment has been minimized.
This comment has been minimized.
Does I think |
This comment has been minimized.
This comment has been minimized.
|
@dtolnay |
This comment has been minimized.
This comment has been minimized.
|
I would love to see this explored in a different serialization library. In the absence of higher rank type parameters in trait bounds, I believe the current approach will continue to serve us well. |
withoutboats commentedSep 19, 2016
•
edited
If I have to conform to some format schema which contains multiple "levels" (e.g. a JSON array inside a JSON object), I have to create a new serializable type for every level in the schema, because
serialize_map_valueandserialize_seq_eltand so on take a typeT: Serialize.It would be great to have higher order versions of every function that takes a
T: Serialize, which instead takes anFn(&mut Self) -> Result<(), Self::Error>, so that these intermediate types could go away. This would also make heterogenous collections much more manageable than they are right now.For example:
Obviously in this case the derived Serialize will suffice, but I have some cases where it won't. I don't know if there are
Serializerimpls that wouldn't be able to define functions like this.