Debugging Techniques
By leveraging impl-side dependencies, CGP providers
are able to include additional dependencies that are not specified in the provider
trait. We have already seen this in action in the previous chapter, for example,
where the provider FormatAsJsonString
is able to require Context
to implement Serialize
, while that is not specified anywhere in the provider
trait StringFormatter
.
We have also went through how provider delegation can be done using
DelegateComponent
, which an aggregated provider like PersonComponents
can use to delegate the implementation of StringFormatter
to FormatAsJsonString
.
Within this delegation, we can also see that the requirement for Context
to implement Serialize
is not required in any part of the code.
In fact, because the provider constraints are not enforced in DelegateComponent
,
the delegation would always be successful, even if some provider constraints
are not satisfied. In other words, the impl-side provider constraints are
enforced lazily in CGP, and compile-time errors would only arise when we
try to use a consumer trait against a concrete context.
Unsatisfied Dependency Errors
To demonstrate how such error would arise, we would reuse the same example
Person
context as the previous chapter.
Consider if we made a mistake and forgot to implement Serialize
for Person
:
#![allow(unused)] fn main() { extern crate anyhow; extern crate serde; extern crate serde_json; extern crate cgp; use cgp::prelude::*; use anyhow::Error; use serde::{Serialize, Deserialize}; #[derive_component(StringFormatterComponent, StringFormatter<Context>)] pub trait CanFormatToString { fn format_to_string(&self) -> Result<String, Error>; } #[derive_component(StringParserComponent, StringParser<Context>)] pub trait CanParseFromString: Sized { fn parse_from_string(raw: &str) -> Result<Self, Error>; } pub struct FormatAsJsonString; impl<Context> StringFormatter<Context> for FormatAsJsonString where Context: Serialize, { fn format_to_string(context: &Context) -> Result<String, Error> { Ok(serde_json::to_string(context)?) } } pub struct ParseFromJsonString; impl<Context> StringParser<Context> for ParseFromJsonString where Context: for<'a> Deserialize<'a>, { fn parse_from_string(json_str: &str) -> Result<Context, Error> { Ok(serde_json::from_str(json_str)?) } } // Note: We forgot to derive Serialize here #[derive(Deserialize, Debug, Eq, PartialEq)] pub struct Person { pub first_name: String, pub last_name: String, } pub struct PersonComponents; impl HasComponents for Person { type Components = PersonComponents; } delegate_components! { PersonComponents { StringFormatterComponent: FormatAsJsonString, StringParserComponent: ParseFromJsonString, } } }
We know that Person
uses PersonComponents
to implement CanFormatToString
,
and PersonComponents
delegates the provider implementation to FormatAsJsonString
.
However, since FormatAsJsonString
requires Person
to implement Serialize
,
without it CanFormatToString
cannot be implemented on PersonContext
.
However, notice that the above code still compiles successfully. This is because we
have not yet try to use CanFormatToString
on person. We can try to add test code to
call format_to_string
, and check if it works:
#![allow(unused)] fn main() { extern crate anyhow; extern crate serde; extern crate serde_json; extern crate cgp; use cgp::prelude::*; use anyhow::Error; use serde::{Serialize, Deserialize}; #[derive_component(StringFormatterComponent, StringFormatter<Context>)] pub trait CanFormatToString { fn format_to_string(&self) -> Result<String, Error>; } #[derive_component(StringParserComponent, StringParser<Context>)] pub trait CanParseFromString: Sized { fn parse_from_string(raw: &str) -> Result<Self, Error>; } pub struct FormatAsJsonString; impl<Context> StringFormatter<Context> for FormatAsJsonString where Context: Serialize, { fn format_to_string(context: &Context) -> Result<String, Error> { Ok(serde_json::to_string(context)?) } } pub struct ParseFromJsonString; impl<Context> StringParser<Context> for ParseFromJsonString where Context: for<'a> Deserialize<'a>, { fn parse_from_string(json_str: &str) -> Result<Context, Error> { Ok(serde_json::from_str(json_str)?) } } // Note: We forgot to derive Serialize here #[derive(Deserialize, Debug, Eq, PartialEq)] pub struct Person { pub first_name: String, pub last_name: String, } pub struct PersonComponents; impl HasComponents for Person { type Components = PersonComponents; } delegate_components! { PersonComponents { StringFormatterComponent: FormatAsJsonString, StringParserComponent: ParseFromJsonString, } } let person = Person { first_name: "John".into(), last_name: "Smith".into() }; println!("{}", person.format_to_string().unwrap()); }
The first time we try to call the method, our code would fail with a compile error that looks like follows:
error[E0599]: the method `format_to_string` exists for struct `Person`, but its trait bounds were not satisfied
|
46 | pub struct Person {
| ----------------- method `format_to_string` not found for this struct because it doesn't satisfy `Person: CanFormatToString`
...
51 | pub struct PersonComponents;
| --------------------------- doesn't satisfy `PersonComponents: StringFormatter<Person>`
...
65 | println!("{}", person.format_to_string().unwrap());
| -------^^^^^^^^^^^^^^^^--
| | |
| | this is an associated function, not a method
| help: use associated function syntax instead: `Person::format_to_string()`
|
= note: found the following associated functions; to be used as methods, functions must have a `self` parameter
note: the candidate is defined in the trait `StringFormatter`
|
14 | fn format_to_string(&self) -> Result<String, Error>;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: trait bound `PersonComponents: StringFormatter<Person>` was not satisfied
|
12 | #[derive_component(StringFormatterComponent, StringFormatter<Context>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: the trait `StringFormatter` must be implemented
|
13 | / pub trait CanFormatToString {
14 | | fn format_to_string(&self) -> Result<String, Error>;
15 | | }
| |_^
= help: items from traits can only be used if the trait is implemented and in scope
note: `CanFormatToString` defines an item `format_to_string`, perhaps you need to implement it
|
13 | pub trait CanFormatToString {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
= note: this error originates in the attribute macro `derive_component` (in Nightly builds, run with -Z macro-backtrace for more info)
Unfortunately, the error message returned from Rust is very confusing, and not
helpful at all in guiding us to the root cause. For an inexperience developer,
the main takeaway from the error message is just that CanFormatString
is
not implemented for Person
, but the developer is left entirely on their
own to find out how to fix it.
One main reason we get such obscured errors is because the implementation of
CanFormatString
is done through two indirect blanket implementations. As Rust
was not originally designed for blanket implementations to be used this way,
it does not follow through to explain why the blanket implementation is not
implemented.
Technically, there is no reason why the Rust compiler cannot be improved to show more detailed errors to make using CGP easier. However, improving the compiler will take time, and we need to present strong argument on why such improvement is needed, e.g. through this book. Until then, we need temporary workarounds to make it easier to debug CGP errors in the meanwhile.
Check Traits
We have learned that CGP lazily resolve dependencies and implements consumer traits on a concrete context only when they are actively used. However, when defining a concrete context, we would like to be able to eagerly check that the consumer traits are implemented, so that no confusing error should arise when the context is being used.
By convention, the best approach for implementing such checks is to define a check trait, which asserts that a concrete context implements all consumer traits that we intended to implement. the check trait would be defined as follows:
#![allow(unused)] fn main() { extern crate anyhow; extern crate serde; extern crate serde_json; extern crate cgp; use cgp::prelude::*; use anyhow::Error; use serde::{Serialize, Deserialize}; #[derive_component(StringFormatterComponent, StringFormatter<Context>)] pub trait CanFormatToString { fn format_to_string(&self) -> Result<String, Error>; } #[derive_component(StringParserComponent, StringParser<Context>)] pub trait CanParseFromString: Sized { fn parse_from_string(raw: &str) -> Result<Self, Error>; } pub struct FormatAsJsonString; impl<Context> StringFormatter<Context> for FormatAsJsonString where Context: Serialize, { fn format_to_string(context: &Context) -> Result<String, Error> { Ok(serde_json::to_string(context)?) } } pub struct ParseFromJsonString; impl<Context> StringParser<Context> for ParseFromJsonString where Context: for<'a> Deserialize<'a>, { fn parse_from_string(json_str: &str) -> Result<Context, Error> { Ok(serde_json::from_str(json_str)?) } } // Note: We forgot to derive Serialize here #[derive(Deserialize, Debug, Eq, PartialEq)] pub struct Person { pub first_name: String, pub last_name: String, } pub struct PersonComponents; impl HasComponents for Person { type Components = PersonComponents; } delegate_components! { PersonComponents { StringFormatterComponent: FormatAsJsonString, StringParserComponent: ParseFromJsonString, } } pub trait CanUsePerson: CanFormatToString + CanParseFromString {} impl CanUsePerson for Person {} }
By convention, a check trait has the name starts with CanUse
, followed by
the name of the concrete context. We list all the consumer traits that
the concrete context should implement as the super trait. The check trait
has an empty body, followed by a blanket implementation for the target
concrete context.
In the example above, we define the check trait CanUsePerson
, which is used
to check that the concrete context Person
implements CanFormatToString
and
CanParseFromString
. If we try to compile the check trait with the
same example code as before, we would get the following error message:
error[E0277]: the trait bound `FormatAsJsonString: StringFormatter<Person>` is not satisfied
|
69 | impl CanUsePerson for Person {}
| ^^^^^^ the trait `StringFormatter<Person>` is not implemented for `FormatAsJsonString`, which is required by `Person: CanFormatToString`
|
= help: the trait `StringFormatter<Context>` is implemented for `FormatAsJsonString`
note: required for `PersonComponents` to implement `StringFormatter<Person>`
|
12 | #[derive_component(StringFormatterComponent, StringFormatter<Context>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required for `Person` to implement `CanFormatToString`
|
12 | #[derive_component(StringFormatterComponent, StringFormatter<Context>)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
note: required by a bound in `CanUsePerson`
|
64 | pub trait CanUsePerson:
| ------------ required by a bound in this trait
65 | CanFormatToString
| ^^^^^^^^^^^^^^^^^ required by this bound in `CanUsePerson`
= note: `CanUsePerson` is a "sealed trait", because to implement it you also need to implement `main::_doctest_main_check_traits_md_229_0::CanFormatToString`, which is not accessible; this is usually done to force you to use one of the provided types that already implement it
= help: the following type implements the trait:
Context
= note: this error originates in the attribute macro `derive_component` (in Nightly builds, run with -Z macro-backtrace for more info)
The error message is still pretty confusing, but it is slightly more informative
than the previous error. Here, we can see that the top of the error says that
StringFormatter<Person>
is not implemented for FormatAsJsonString
.
Although it does not point to the root cause, at least we are guided to look
into the implementation of FormatAsJsonString
to find out what went wrong there.
At the moment, there is no better way to simplify debugging further, and
we need to manually look into FormatAsJsonString
to check why it could not
implement StringFormatter
for the Person
context. Here, the important
thing to look for is the additional constraints that FormatAsJsonString
requires on the context, which in this case is Serialize
.
We can make use of the check trait to further track down all the indirect
dependencies of the providers that the concrete context uses. In this case,
we determine that Serialize
is needed for FormatAsJsonString
, and
Deserialize
is needed for ParseFromJsonString
. So we add them into
the super trait of CanUsePerson
as follows:
#![allow(unused)] fn main() { extern crate anyhow; extern crate serde; extern crate serde_json; extern crate cgp; use cgp::prelude::*; use anyhow::Error; use serde::{Serialize, Deserialize}; #[derive_component(StringFormatterComponent, StringFormatter<Context>)] pub trait CanFormatToString { fn format_to_string(&self) -> Result<String, Error>; } #[derive_component(StringParserComponent, StringParser<Context>)] pub trait CanParseFromString: Sized { fn parse_from_string(raw: &str) -> Result<Self, Error>; } pub struct FormatAsJsonString; impl<Context> StringFormatter<Context> for FormatAsJsonString where Context: Serialize, { fn format_to_string(context: &Context) -> Result<String, Error> { Ok(serde_json::to_string(context)?) } } pub struct ParseFromJsonString; impl<Context> StringParser<Context> for ParseFromJsonString where Context: for<'a> Deserialize<'a>, { fn parse_from_string(json_str: &str) -> Result<Context, Error> { Ok(serde_json::from_str(json_str)?) } } // Note: We forgot to derive Serialize here #[derive(Deserialize, Debug, Eq, PartialEq)] pub struct Person { pub first_name: String, pub last_name: String, } pub struct PersonComponents; impl HasComponents for Person { type Components = PersonComponents; } delegate_components! { PersonComponents { StringFormatterComponent: FormatAsJsonString, StringParserComponent: ParseFromJsonString, } } pub trait CanUsePerson: Serialize + for<'a> Deserialize<'a> + CanFormatToString + CanParseFromString {} impl CanUsePerson for Person {} }
When we try to compile CanUsePerson
again, we would see a different error
message at the top:
error[E0277]: the trait bound `Person: Serialize` is not satisfied
|
71 | impl CanUsePerson for Person {}
| ^^^^^^ the trait `Serialize` is not implemented for `Person`
|
= note: for local types consider adding `#[derive(serde::Serialize)]` to your `Person` type
= note: for types from other crates check whether the crate offers a `serde` feature flag
This tells us that we have forgotten to implement Serialize
for Person.
We can then take our action to properly fill in the missing dependencies.
Debugging Check Traits
Due to the need for check traits, the work for implementing a concrete context often involves wiring up the providers, and then checking that the providers and all their dependencies are implemented. As the number of components increase, the number of dependencies we need to check also increase accordingly.
When encountering errors in the check traits, it often helps to comment out a large
portion of the dependencies, to focus on resolving the errors arise from a specific
dependency. For example, in the check trait we can temporarily check for the
implementation of CanFormatToString
by commenting out all other constraints as follows:
pub trait CanUsePerson:
Sized
// + Serialize
// + for<'a> Deserialize<'a>
+ CanFormatToString
// + CanParseFromString
{}
We add a dummy constaint like Sized
in the beginning of the super traits for
CanUsePerson
, so that we can easily comment out individual lines and not worry
about whether it would lead to a dangling +
sign.
We can then pin point the error to a specific provider, and then continue
tracing the missing dependencies from there. We would then notice that
FormatAsJsonString
requires Serialize
, which we can then update the
commented code to:
pub trait CanUsePerson:
Sized
+ Serialize
// + for<'a> Deserialize<'a>
// + CanFormatToString
// + CanParseFromString
{}
This technique can hopefully help speed up the debugging process, and determine which dependency is missing.
Future Improvements
The need of manual debugging using check traits is probably one of the major blockers for spreading CGP for wider adoption. Although it is not technically an unsolvable problem, it is a matter of allocating sufficient time and resource to improve the error messages from Rust.
When the opportunity arise, we plan to eventually work on submitting pull requests for improving the error messages when the constraints from blanket implementations cannot be satisfied. This book will be updated once we get an experimental version of the Rust compiler working with improved error messages.
We also consider exploring the option of building a custom compiler plugin similar to Clippy, which can be used to explain CGP-related errors in more direct ways. Similarly, it should not be too challenging to build IDE extensions similar to Rust Analyzer, which can provide more help in fixing CGP-related errors.
Until improved tooling becomes available, we hope that the use of check traits for debugging is at least sufficient for early adopters. From this chapter onward, we are just starting to explore what can be done with the basic framework of CGP in place.