4

I am curious to see how much boilerplate one can save through built-in reflection.

A little background

My idea behind structured logging is to use various small tailored types to separate content from representation. Instead of unstructured logger.info("Found a bar with {} foos", bar.foo) one uses something like logger.info(FoundBar{ _bar: bar })

My Rust-ish approach

  • define a Log trait
  • provide a default implementation that calls the Serde machinery to serialize the type (to JSON in this example)
  • define loggable types easily by letting them "inherit" the default implementation
  • profit

Define the trait, providing a default impl:

trait Log { fn to_log(&self) -> String { serde_json::to_string(&self).unwrap() } } 

(RLS is already drawing angry red squiggles, but bear with me)

Define a simple type to be logged:

#[derive(Serialize)] struct Message { msg: String, } 

and let it use the default implementation:

impl Log for Message {} 

and finally the polymorphic logging function defined in terms of the trait:

fn log(log: &Log) { println!("serialized = {}", log.to_log()); } 

The compiler complains:

error[E0277]: the trait bound `Self: _IMPL_DESERIALIZE_FOR_Message::_serde::Serialize` is not satisfied --> src\main.rs:8:9 | 8 | serde_json::to_string(&self).unwrap() | ^^^^^^^^^^^^^^^^^^^^^ the trait `_IMPL_DESERIALIZE_FOR_Message::_serde::Serialize` is not implemented for `Self` | = help: consider adding a `where Self: _IMPL_DESERIALIZE_FOR_Message::_serde::Serialize` bound = note: required because of the requirements on the impl of `_IMPL_DESERIALIZE_FOR_Message::_serde::Serialize` for `&Self` = note: required by `serde_json::ser::to_string` 

Adding the where Self suggestion to my trait function only produces different errors (error[E0433]: failed to resolve. Use of undeclared type or module _IMPL_DESERIALIZE_FOR_Message), but apart from that it seems like a Bad Idea(TM) to have this implementation detail of Serde leak into my code.

How do I portably constrain my trait (using where?) to only apply to types that have the correct derive? Even better, can I "inject" the derive functionality into types using the trait?

2
  • @Shepmaster good remark: in hindsight the question should have been "[...] to implement serde::Serialize", which amounts to half an answer already... Commented Nov 26, 2018 at 20:58
  • 1
    I was hoping that was what you meant. I don't see any harm in having a more accurate title to catch future searchers. Commented Nov 26, 2018 at 21:00

2 Answers 2

2

If you create a MCVE of your problem on the playground, you get a more accurate error:

error[E0277]: the trait bound `Self: serde::Serialize` is not satisfied --> src/lib.rs:6:9 | 6 | serde_json::to_string(&self).unwrap() | ^^^^^^^^^^^^^^^^^^^^^ the trait `serde::Serialize` is not implemented for `Self` | = help: consider adding a `where Self: serde::Serialize` bound = note: required because of the requirements on the impl of `serde::Serialize` for `&Self` = note: required by `serde_json::ser::to_string` 

Following the suggestion, but using the idiomatic supertrait syntax, answers your question:

trait Log: serde::Serialize { fn to_log(&self) -> String { serde_json::to_string(&self).unwrap() } } 

You'll need to change your log function for object-safety reasons:

fn log(log: &impl Log) { println!("serialized = {}", log.to_log()); } 

See also:

Sign up to request clarification or add additional context in comments.

4 Comments

How come the playground error message is clearer? Newer (unstable?) version of Rust/compiler? I am on stable-x86_64-pc-windows-msvc, which uses rustc 1.30.1
@dlw I assume it's the minimal aspect. The playground is also using 1.30.1 (at the moment), but this example only has two crates in use.
Could you expand on the "object-safety reasons"? Why would I want to borrow from the unnamed object instead of passing ownership to log()? [unnamed object resulting from calls like logger.info(FoundBar{ _bar: bar })]
@dlw Could you expand on — that's what all 4 of the linked Q&A are about; I didn't want to further duplicate existing information. Why would I want to borrow — that's up to you. Your original code took a reference trait object (&Log), so switching to a reference to a generic type was the smallest change. You could also pick impl Log.
1

Using trait inheritance works, but using the right Serde trait, not the compiler-suggested one:

trait Log: serde::Serialize { fn to_log(&self) -> String { serde_json::to_string(&self).unwrap() } } 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.