Introduction

This book covers the design patterns for context-generic programming (CGP), a new programming paradigm for Rust that allows strongly-typed components to be implemented and composed in a modular, generic, and type-safe way.

What is Context-Generic Programming

A high level overview of CGP is available on the project website. This section contains a summarized version of the overview.

At its core, CGP makes use of Rust's trait system to build generic component interfaces that decouple code that consumes an interface from code that implements an interface. Through this decoupling, code can be written to be generic over any context, and then be wired to be used on a concrete context by writing few lines of code. CGP makes use of Rust's strong type system to help ensure that any such wiring is type-safe, catching any unsatisfied dependencies as compile-time errors.

CGP shares some similarities with other modular programming patterns, such as OCaml modules, Scala implicits, mixins, and dependency injection. Compared to these other patterns, CGP has a unique advantage that it enables high modularity while also being type-safe and concise. With Rust as its host language, CGP also allows high-performance and low-level code to be written in a modular way, without requiring complex runtime support.

CGP is designed to solve a wide range of common problems in programming, including error handling, logging, encoding, and modular dispatch. In particular, it allows writing static-typed Rust programs to be almost as expressive as writing dynamic-typed programs, but with the additional type safety guarantees. If you ever feel that Rust's type system is restricting your ability to reuse code, or be forced to duplicate code through copy-pasting or macros, then CGP may be of help to solve your problem.

That said, programming in CGP is as expressive, but not as easy, as dynamic-typed programming. There may be a steep learning curve in learning how to program in a generic way, and this book aims to help make that learning process more approachable. Thoughout this book, we will slowly understand how CGP works, and learn about useful design patterns that can be used in any programming situation.

Work In Progress

This book is currently a work in progress. A majority of the chapter is yet to be written. Please come back later to check out a completed version of this book.

Scope of This Book

This book is written in the style of a reference material for readers with all levels of expertise. As a result, it may not be as easy to understand for beginners who need a little more introduction to more basic programming techniques in Rust.

A separate book will be written in the future, to provide beginner-friendly tutorials for learning context-generic programming.

For brievity, this book also does not cover motivation or concrete examples of why you should learn and use context-generic programming. We will cover that in blog posts, and a separate book that covers real world use of CGP.

Chapter Outlines

The first section of this book, Terminology, will introduce common terms to be used to understand CGP. We will learn about what is a context, and what are consumer and provider traits.

In the next section, Core Concepts, we will cover the core concepts and the Rust-specific design patterns that we use to enable context-generic programming.

Following that, Design Patterns will introduce general design patterns that are built on top of the foundation of context-generic programming.

The section Domain-Specific Patterns will cover use-case-specific design patterns, such as error handling and logging.

Finally, the secion Related Concepts will compare context-generic programming with related concepts, such as the similarity and differences of context-generic programming as compared to object-oriented programming.

Contribution

This book is open sourced under the MIT license on GitHub.

Anyone is welcome to contribute by submitting pull requests for grammatical correction, content improvement, or adding new design patterns.

A GitHub Discussions forum is available for readers to ask questions or have discussions for topics covered in this book.

Context

In CGP, we use the term context to refer to a type that provide certain functionalities, or dependencies. The most common kind of functionality a context may provide is a method.

Following is a simple hello world example of a context providing a method:

#![allow(unused)]
fn main() {
struct MyContext;

impl MyContext {
    fn hello(&self) {
        println!("Hello World!");
    }
}
}

The example above is mostly self explanatory. We first define a struct called MyContext, followed by an impl block for MyContext. Inside the impl block, a hello method is provided, which prints out "Hello World!" to the terminal when called.

We can then use the hello method anywhere that we have a value of type MyContext, such as:

#![allow(unused)]
fn main() {
struct MyContext;

impl MyContext {
    fn hello(&self) {
        println!("Hello World!");
    }
}

let my_context = MyContext;

my_context.hello();
}

Contexts vs Classes

The above example may seem trivial for most programmers, especially for those who come from object-oriented programming (OOP) background. In fact, one way we can have a simplified view of a context is that it is similar to OOP concepts such as classes, objects, and interfaces.

Beyond the surface-level similarity, the concept of contexts in CGP is more general than classes and other similar concepts. As a result, it is useful to think of the term context as a new concept that we will learn in this book.

As we will learn in later chapters, aside from methods, a context may provide other kinds of functionalities, such as associated types and constants.

Contexts vs Types

Although a context is usually made of a type, in CGP we do not treat all types as contexts. Instead, we expect CGP contexts to offer some level of modularity, which can be achived by using the programming patterns introduced in this book.

Consumer

In CGP, a consumer is a piece of code that consumes certain functionalities from a context. There are several ways which a consumer may consume a functionality. At its most basic, if a consumer has access to the concrete type of a context, it can access any methods defined by an impl block of that context.

#![allow(unused)]
fn main() {
struct Person { name: String }

impl Person {
    fn name(&self) -> &str {
        &self.name
    }
}

fn greet(person: &Person) {
    println!("Hello, {}!", person.name());
}
}

in the above example, we have a greet function that prints a greeting to a person using the method Person::name. In other words, we say that the greet function is a consumer to the Person::name method.

Context-Generic Consumers

The greet function in our previous example can only work with the Person struct. However, if we inspect the implementation of greet, we can see that it is possible to generalize greet to work with any type that has a name.

To generalize greet, we first need to define a trait that acts as an interface for getting a name:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

fn greet<Context>(context: &Context)
where
    Context: HasName
{
    println!("Hello, {}", context.name());
}
}

In the example above, we define a HasName trait that provides a name method. We then redefine greet to work generically with any Context type, with the where clause requiring Context to implement HasName. Inside the function body, we call the name method, and print out the greeting of that name.

Notice that in this example, we are able to implement greet before we have any concrete implementation of HasName. Compared to before, greet is now decoupled from the Person type, thus making our code more modular.

In CGP, this new version of greet is considered a context-generic consumer, as it is able to generically consume the HasName::name method from any Context type that implements HasName.

The concept of context-generic consumer is not unique to CGP. In fact, it is already commonly used in most of the Rust code that uses traits. However, we make an effort to study this concept, so that we can further generalize the concept in the later chapters of this book.

Provider

In CGP, a provider is a piece of code that implements certain functionality for a context. At its most basic, a provider is consist of an impl block for a trait.

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}
}

In the above example, we implement the HasName for the Person struct. The block impl HasName for Person is a provider of the HasName trait for the Person context.

Similar to the concept of a consumer, the use of provider is common in any Rust code that implements a trait. However, compared to cosumers, there are limitations on how providers can be defined in Rust.

For this example, the impl block is a context-specific provider for the Person context. Furthermore, due to the restrictions of Rust's trait system, there can be at most one provider of HasName for the Person context. Another common restriction is that the provider has to be defined in the same crate as either the trait or the context.

The asymetry between what can be done with a provider, as compared to a consumer, is often a source of complexity in many Rust programs. As we will learn in later chapters, one of the goals of CGP is to break this asymetry, and make it easy to implement context-generic providers.

Providers as Consumers

Although we have providers and consumers as distinct concepts, it is common to have code that serve as both providers and consumers.

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}

trait CanGreet {
    fn greet(&self);
}

impl CanGreet for Person {
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}
}

The example above shows a new CanGreet trait, which provides a greet method. We then implement CanGreet for Person, with the greet implementation using self.name() to print out the name to be greeted.

Here, the block impl CanGreet for Person is a provider of CanGreet for the Person context. At the same time, it is also the consumer of HasName for the Person context. In terms of genericity, the example code is context-specific to the Person context for both the consumer and provider side.

As we will see in later chapters, a powerful idea introduced by CGP is that a piece of code can have multiple spectrums of genericity on the consumer and provider sides.

Blanket Trait Implementations

In the previous chapter, we have an implementation of CanGreet for Person that makes use of HasName to retrieve the person's name to be printed. However, the implementation is context-specific to the Person context, and cannot be reused for other contexts.

Ideally, we want to be able to define context-generic implementations of Greet that works with any context type that also implements HasName. For this, the blanket trait implementations pattern is one basic way which we can use for defining context-generic implementations:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}
}

The above example shows a blanket trait implementation of CanGreet for any Context type that implements HasName. With that, contexts like Person do not need to explicitly implement CanGreet, if they already implement HasName:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}

let person = Person { name: "Alice".to_owned() };
person.greet();
}

As shown above, we are able to call person.greet() without having a context-specific implementation of CanGreet for Person.

Extension Traits

The use of blanket trait implementation is commonly found in many Rust libraries today. For example, Itertools provides a blanket implementation for any context that implements Iterator. Another example is StreamExt, which is implemented for any context that implements Stream.

Traits such as Itertools and StreamExt are sometimes known as extension traits. This is because the purpose of the trait is to extend the behavior of existing types, without having to own the type or base traits. While the use of extension traits is a common use case for blanket implementations, there are other ways we can make use of blanket implementations.

Overriding Blanket Implementations

Traits containing blanket implementation are usually not meant to be implemented manually by individual contexts. They are usually meant to serve as convenient methods that extends the functionality of another trait. However, Rust's trait system does not completely prevent us from overriding the blanket implementation.

Supposed that we have a VipPerson context that we want to implement a different way of greeting the VIP person. We could override the implementation as follows:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct VipPerson { name: String, /* other fields */ }

impl CanGreet for VipPerson {
    fn greet(&self) {
        println!("A warm welcome to you, {}!", self.name);
    }
}
}

The example above shows two providers of CanGreet. The first provider is a context-generic provider that we covered previously, but the second provider is a context-specific provider for the VipPerson context.

Conflicting Implementations

In the previous example, we are able to define a custom provider for VipPerson, but with an important caveat: that VipPerson does not implement HasName. If we try to define a custom provider for contexts that already implement HasName, such as for Person, the compilation would fail:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

struct Person { name: String }

impl HasName for Person {
    fn name(&self) -> &str {
        &self.name
    }
}

impl CanGreet for Person {
    fn greet(&self) {
        println!("Hi, {}!", self.name());
    }
}
}

If we try to compile the example code above, we would get an error with the message:

conflicting implementations of trait `CanGreet` for type `Person`

The reason for the conflict is because Rust trait system requires all types to have unambigious implementation of any given trait. To see why such requirement is necessary, consider the following example:

#![allow(unused)]
fn main() {
trait HasName {
    fn name(&self) -> &str;
}

trait CanGreet {
    fn greet(&self);
}

impl<Context> CanGreet for Context
where
    Context: HasName,
{
    fn greet(&self) {
        println!("Hello, {}!", self.name());
    }
}

fn call_greet_generically<Context>(context: &Context)
where
    Context: HasName,
{
    context.greet()
}
}

The example above shows a generic function call_greet_generically, which work with any Context that implements HasName. Even though it does not require Context to implement CanGreet, it nevertheless can call context.greet(). This is because with the guarantee from Rust's trait system, the compiler can always safely use the blanket implementation of CanGreet during compilation.

If Rust were to allow ambiguous override of blanket implementations, such as what we tried with Person, it would have resulted in inconsistencies in the compiled code, depending on whether it is known that the generic type is instantiated to Person.

Note that in general, it is not always possible to know locally about the concrete type that is instantiated in a generic code. This is because a generic function like call_greet_generically can once again be called by other generic code. This is why even though there are unstable Rust features such as trait specialization, such feature has to be carefully considered to ensure that no inconsistency can arise.

Limitations of Blanket Implementations

Due to potential conflicting implementations, the use of blanket implementations offer limited customizability, in case if a context wants to have a different implementation. Although a context many define its own context-specific provider to override the blanket provider, it would face other limitations such as not being able to implement other traits that may cause a conflict.

In practice, we consider that blanket implementations allow for a singular context-generic provider to be defined. In future chapters, we will look at how to relax the singular constraint, to make it possible to allow multiple context-generic or context-specific providers to co-exist.

Impl-side Dependencies

When writing generic code, we often need to specify the trait bounds that we would like to use with a generic type. However, when the trait bounds involve traits that make use of blanket implementations, there are different ways that we can specify the trait bounds.

Supposed that we want to define a generic function that formats a list of items into a comma-separated string. Our generic function could make use the Itertools::join to format an iterator. Our first attempt would be to define our generic function as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

fn format_iter<I>(mut iter: I) -> String
where
    I: Iterator,
    I::Item: Display,
{
    iter.join(", ")
}

assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3");
}

The generic function format_iter takes a generic type I that implements Iterator. Additionally, we require I::Item to implement Display. With both constraints in place, we are able to call Itertools::join inside the generic function to join the items using ", " as separator.

In the above example, we are able to use the method from Itertools on I, even though we do not specify the constraint I: Itertools in our where clause. This is made possible because the trait Itertools has a blanket implementation on all types that implement Iterator, including the case when we do not know the concrete type behind I. Additionally, the method Itertools::join requires I::Item to implement Display, so we also include the constraint in our where clause.

When using traits that have blanket implementation, we can also go the other way and require I to implement Itertools instead of Iterator:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

fn format_iter<I>(mut items: I) -> String
where
    I: Itertools,
    I::Item: Display,
{
    items.join(", ")
}

assert_eq!(format_iter(vec![1, 2, 3].into_iter()), "1, 2, 3");
}

By doing so, we make it explicit of the intention that we only care that I implements Itertools, and hide the fact that we need I to also implement Iterator in order to implement Itertools.

Constraint Leaks

At this point, we have defined our generic function format_iter with two constraints in the where clause. When calling format_iter from another generic function, the constraint would also be propagated to the caller.

As a demonstration, supposed that we want to define another generic function that uses format_iter to format any type that implements IntoIterator, we would need to also include the constraints needed by format_iter as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

fn format_iter<I>(mut items: I) -> String
where
    I: Itertools,
    I::Item: Display,
{
    items.join(", ")
}

fn format_items<C>(items: C) -> String
where
    C: IntoIterator,
    C::IntoIter: Itertools,
    <C::IntoIter as Iterator>::Item: Display,
{
    format_iter(items.into_iter())
}

assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3");
}

When defining the generic function format_items above, we only really care that the generic type C implements IntoIterator, and then pass C::IntoIter to format_iter. However, because of the constraints specified by format_iter, Rust also forces us to specify the same constraints in format_items, even if we don't need the constraints directly.

As we can see, the constraints specified in the where clause of format_iter is a form of leaky abstraction, as it also forces generic consumers like format_items to also know about the internal details of how format_iter uses the iterator.

The leaking of where constraints also makes it challenging to write highly generic functions at a larger scale. The number of constraints could quickly become unmanageable, if a high level generic function calls many low-level generic functions that each has different constraints.

Furthermore, the repeatedly specified constraints become tightly coupled with the concrete implementation of the low-level functions. For example, if format_iter changed from using Itertools::join to other ways of formatting the iterator, the constraints would become outdated and need to be changed in format_items.

Hiding Constraints with Traits and Blanket Implementations

Using the techniques we learned from blanket implementations, there is a way to hide the where constraints by redefining our generic functions as traits with blanket implementations.

We would first rewrite format_iter into a trait CanFormatIter as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

assert_eq!(vec![1, 2, 3].into_iter().format_iter(), "1, 2, 3");
}

The trait CanFormatIter is defined with a single method, format_iter, which consumes self and return a String. The trait comes with a blanket implementation for any type I, with the constraints that I: Itertools and I::Item: Display. Following that, we have the same implementation as before, which calls Itertools::join to format the iterator as a comma-separated string. By having a blanket implementation, we signal that CanFormatIter is intended to be derived automatically, and that no explicit implementation is required.

It is worth noting that the constraints I: Itertools and I::Item: Display are only present at the impl block, but not at the trait definition of CanFormatIter. By doing so, we have effectively "hidden" the constraints inside the impl block, and prevent it from leaking to its consumers.

We can now refactor format_items to use CanFormatIter as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

fn format_items<C>(items: C) -> String
where
    C: IntoIterator,
    C::IntoIter: CanFormatIter,
{
    items.into_iter().format_iter()
}

assert_eq!(format_items(&vec![1, 2, 3]), "1, 2, 3");
}

In the new version of format_items, our where constraints are now simplified to only require C::IntoIter to implement CanFormatIter. With that, we are able to make it explicit that format_items needs CanFormatIter to be implemented, but it doesn't matter how it is implemented.

The reason this technique works is similar to how we used Itertools in our previous examples. At the call site of the code that calls format_items, Rust would see that the generic function requires C::IntoIter to implement CanFormatIter. But at the same time, Rust also sees that CanFormatIter has a blanket implementation. So if the constraints specified at the blanket implementation are satisfied, Rust would automatically provide an implementation of CanFormatIter to format_items, without the caller needing to know how that is done.

Nested Constraints Hiding

Once we have seen in action how we can hide constraints behind the blanket impl blocks of traits, there is no stopping for us to define more traits with blanket implementations to hide even more constraints.

For instance, we could rewrite format_items into a CanFormatItems trait as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

assert_eq!(vec![1, 2, 3].format_items(), "1, 2, 3");
}

We first define a CanFormatItems trait, with a method format_items(&self). Here, we make an improvement over the original function to allow a reference &self, instead of an owned value self. This allows a container such as Vec to not be consumed when we try to format its items, which would be unnecessarily inefficient.

Inside the blanket impl block for CanFormatItems, we define it to work with any Context type, given that the generic Context type implements some constraints with higher ranked trait bounds (HRTB). While HRTB is an advanced subject on its own, the general idea is that we require that any reference &'a Context with any lifetime 'a implements IntoIterator. This is so that when IntoIterator::into_iter is called, the Self type being consumed is the reference type &'a Context, which is implicitly copyable, and thus allow the same context to be reused later on at other places.

Additionally, we require that <&'a Context as IntoIterator>::IntoIter implements CanFormatIter, so that we can call its method on the produced iterator. Thanks to the hiding of constraints by CanFormatIter, we can avoid specifying an overly verbose constraint that the iterator item also needs to implement Display.

Individually, the constraints hidden by CanFormatIter and CanFormatItems may not look significant. But when combining together, we can see how isolating the constraints help us better organize our code and make them cleaner. In particular, we can now write generic functions that consume CanFormatIter without having to understand all the indirect constraints underneath.

To demonstrate, supposed that we want to compare two list of items and see whether they have the same string representation. We can now define a generic stringly_equals function as follows:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

fn stringly_equals<Context>(left: &Context, right: &Context) -> bool
where
    Context: CanFormatItems,
{
    left.format_items() == right.format_items()
}

assert_eq!(stringly_equals(&vec![1, 2, 3], &vec![1, 2, 4]), false);
}

Our generic function stringly_equals can now be defined cleanly to work over any Context type that implements CanFormatItems. In this case, the function does not even need to be aware that Context needs to produce an iterator, with its items implementing Display.

Furthermore, instead of defining a generic function, we could instead use the same programming technique and define a CanStringlyCompareItems trait that does the same thing:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

pub trait CanStringlyCompareItems {
    fn stringly_equals(&self, other: &Self) -> bool;
}

impl<Context> CanStringlyCompareItems for Context
where
    Context: CanFormatItems,
{
    fn stringly_equals(&self, other: &Self) -> bool {
        self.format_items() == other.format_items()
    }
}

assert_eq!(vec![1, 2, 3].stringly_equals(&vec![1, 2, 4]), false);
}

For each new trait we layer on top, we can build higher level interfaces that hide away lower level implementation details. When CanStringlyCompareItems is used, the consumer is shielded away from knowing anything about the concrete context, other than that two values are be compared by first being formatted into strings.

The example here may seem a bit stupid, but there are some practical use cases of implementing comparing two values as strings. For instance, a serialization library may want to use it inside tests to check whether two different values are serialized into the same string. For such use case, we may want to define another trait to help make such assertion during tests:

#![allow(unused)]
fn main() {
extern crate core;
extern crate itertools;

use core::fmt::Display;
use itertools::Itertools;

pub trait CanFormatIter {
    fn format_iter(self) -> String;
}

impl<I> CanFormatIter for I
where
    I: Itertools,
    I::Item: Display,
{
    fn format_iter(mut self) -> String
    {
        self.join(", ")
    }
}

pub trait CanFormatItems {
    fn format_items(&self) -> String;
}

impl<Context> CanFormatItems for Context
where
    for<'a> &'a Context: IntoIterator,
    for<'a> <&'a Context as IntoIterator>::IntoIter: CanFormatIter,
{
    fn format_items(&self) -> String
    {
        self.into_iter().format_iter()
    }
}

pub trait CanStringlyCompareItems {
    fn stringly_equals(&self, other: &Self) -> bool;
}

impl<Context> CanStringlyCompareItems for Context
where
    Context: CanFormatItems,
{
    fn stringly_equals(&self, other: &Self) -> bool {
        self.format_items() == other.format_items()
    }
}

pub trait CanAssertEqualImpliesStringlyEqual {
    fn assert_equal_implies_stringly_equal(&self, other: &Self);
}

impl<Context> CanAssertEqualImpliesStringlyEqual for Context
where
    Context: Eq + CanStringlyCompareItems,
{
    fn assert_equal_implies_stringly_equal(&self, other: &Self) {
        assert_eq!(self == other, self.stringly_equals(other))
    }
}

vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 3]);
vec![1, 2, 3].assert_equal_implies_stringly_equal(&vec![1, 2, 4]);
}

The trait CanAssertEqualImpliesStringlyEqual provides a method that takes two contexts of the same type, and assert that if both context values are equal, then their string representation are also equal. Inside the blanket impl block, we require that Context implements CanStringlyCompareItems, as well as Eq.

Thanks to the hiding of constraints, the trait CanAssertEqualImpliesStringlyEqual can cleanly separate its direct dependencies, Eq, from the rest of the indirect dependencies.

Dependency Injection

The programming technique that we have introduced in this chapter is sometimes known as dependency injection in some other languages and programming paradigms. The general idea is that the impl blocks are able to specify the dependencies they need in the form of where constraints, and the Rust trait system automatically helps us to resolve the dependencies at compile time.

In context-generic programming, we think of constraints in the impl blocks not as constraints, but more generally dependencies that the concrete implementation needs. Each time a new trait is defined, it serves as an interface for consumers to include them as a dependency, but at the same time separates the declaration from the concrete implementations.

Provider Traits

In the previous chapters on blanket implementations and impl-side dependencies, we learned about the power of using blanket impl blocks to simplify and hide the dependencies required by each part of the implementation. However, one major limitation of blanket implementations is that there cannot be multiple potentially overlapping implementations, due to restrictions in Rust's trait system. In CGP, we can overcome this limitation by introducing the concept of provider traits.

The main idea behind provider traits is to define Rust traits that are dedicated for providers to define new implementations, and separate it from the consumer traits that are more suitable for consumers that use the traits. Consider a simple consumer trait CanFormatToString, which allows formatting a context into string:

#![allow(unused)]
fn main() {
pub trait CanFormatString {
    fn format_string(&self) -> String;
}
}

The trait we defined here is almost identical to the standard library's ToString trait. But we will duplicate the trait here to tweak how it is implemented. We first note that the original ToString trait has a blanket implementation for any type that implements Display:

#![allow(unused)]
fn main() {
use core::fmt::Display;

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: Display,
{
    fn format_string(&self) -> String {
        format!("{}", self)
    }
}
}

Although having this blanket implementation is convenient, it restricts us from being able to format the context in other ways, such as using Debug.

#![allow(unused)]
fn main() {
use core::fmt::{Display, Debug};

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: Display,
{
    fn format_string(&self) -> String {
        format!("{}", self)
    }
}

// Error: conflicting implementation
impl<Context> CanFormatString for Context
where
    Context: Debug,
{
    fn format_string(&self) -> String {
        format!("{:?}", self)
    }
}
}

To overcome this limitation, we can introduce a provider trait that we'd call StringFormatter, which we will then use for defining implementations:

#![allow(unused)]
fn main() {
pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}
}

Compared to CanFormatString, the trait StringFormatter replaces the implicit context type Self with an explicit context type Context, as defined in its type parameter. Following that, it replaces all occurrances of &self with context: &Context.

By avoiding the use of Self in provider traits, we can bypass the restrictions of Rust trait system, and have multiple implementations defined. Continuing the earlier example, we can define the Display and Debug implementations of CanFormatString as two separate providers of StringFormatter:

#![allow(unused)]
fn main() {
use core::fmt::{Display, Debug};

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

pub struct FormatStringWithDisplay;

pub struct FormatStringWithDebug;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}

impl<Context> StringFormatter<Context> for FormatStringWithDebug
where
    Context: Debug,
{
    fn format_string(context: &Context) -> String {
        format!("{:?}", context)
    }
}
}

With provider traits, we now have two named providers FormatStringWithDisplay and FormatStringWithDebug, which are defined as dummy structs. These structs are not meant to be used inside any code during run time. Rather, they are used as identifiers at the type level for us to refer to the providers during compile time.

Notice that inside the implementation of StringFormatter, the types FormatStringWithDisplay and FormatStringWithDebug are in the position that is typically used for Self, but we don't use Self anywhere in the implementation. Instead, the original Self type is now referred explicitly as the Context type, and we use &context instead of &self inside the implementation.

From the point of view of Rust's trait system, the rules for overlapping implementation only applies to the Self type. But because we have two distinct Self types here (FormatStringWithDisplay and FormatStringWithDebug), the two implementations are not considered overlapping, and we are able to define them without any compilation error.

Using Provider Traits Directly

Although provider traits allow us to define overlapping implementations, the main downside is that consumer code cannot make use of an implementation without explicitly choosing the implementation.

Consider the following Person context defined:

#![allow(unused)]
fn main() {
use core::fmt::{self, Display, Debug};

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

pub struct FormatStringWithDisplay;

pub struct FormatStringWithDebug;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}

impl<Context> StringFormatter<Context> for FormatStringWithDebug
where
    Context: Debug,
{
    fn format_string(context: &Context) -> String {
        format!("{:?}", context)
    }
}

#[derive(Debug)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

impl Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.first_name, self.last_name)
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };

assert_eq!(
    FormatStringWithDisplay::format_string(&person),
    "John Smith"
);

assert_eq!(
    FormatStringWithDebug::format_string(&person),
    "Person { first_name: \"John\", last_name: \"Smith\" }"
);
}

Our Person struct is defined with both Debug and Display implementations. When using format_string on a value person: Person, we cannot just call person.format_string(). Instead, we have to explicitly pick a provider Provider, and call it with Provider::format_string(&person). On the other hand, thanks to the explicit syntax, we can use both FormatStringWithDisplay and FormatStringWithDebug on Person without any issue.

Nevertheless, having to explicitly pick a provider can be problematic, especially if there are multiple providers to choose from. In the next chapter, we will look at how we can link a provider trait with a consumer trait, so that we can use back the simple person.format_string() syntax without needing to know which provider to choose from.

Beyond String Formatting

In this chapter, we make use of a very simplified example of formatting strings to demonstrate the use case of provider traits. Our example may seem a bit redundant, as it does not simplify the code much as compared to directly using format!() to format the string with either Debug or Display. However, the provider trait allows us to also define providers that format a context in other ways, such as by serializing it as JSON:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> Result<String, Error>;
}

use anyhow::Error;
use serde::Serialize;
use serde_json::to_string;

pub struct FormatAsJson;

impl<Context> StringFormatter<Context> for FormatAsJson
where
    Context: Serialize,
{
    fn format_string(context: &Context) -> Result<String, Error> {
        Ok(to_string(context)?)
    }
}
}

To allow for error handling, we update the method signature of format_string to return a Result, with anyhow::Error being used as a general error type. We will also be covering better ways to handle errors in a context-generic way in in later chapters.

If we recall from the previous chapter, the CanFormatIter trait in fact has the same method signature as StringFormatter. So we can refactor the code from the previous chapter, and turn it into a context-generic provider that works for any iterable context like Vec.

This use of provider traits can also be more useful in more complex use cases, such as implementing Serialize, or even the Display trait itself. If we were to implement these traits using CGP, we would also define provider traits such as follows:

#![allow(unused)]
fn main() {
extern crate serde;

use core::fmt;
use serde::Serializer;

pub trait ProvideSerialize<Context> {
    fn serialize<S: Serializer>(context: &Context, serializer: S) -> Result<S::Ok, S::Error>;
}

pub trait ProvideFormat<Context> {
    fn fmt(context: &Context, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error>;
}
}

As we can see above, we can define provider traits for any existing traits by replacing the Self type with an explicit Context type. In this chapter, we would not be covering the details on how to use CGP and provider traits to simplify formatting and serialization implementations, as that is beyond the current scope. Suffice to say, as we go through later chapters, it will become clearer on how having provider traits can impact us on thinking about how to structure and implement modular code.

Linking Consumers with Providers

In the previous chapter, we learned about how provider traits allow multiple overlapping implementations to be defined. However, if everything is implemented only as provider traits, it would be much more tedious having to determine which provider to use, at every time when we need to use the trait. To overcome this, we would need have both provider traits and consumer traits, and have some ways to choose a provider when implementing a consumer trait.

Implementing Consumer Traits

The simplest way to link a consumer trait with a provider is by implementing the consumer trait to call a chosen provider. Consider the StringFormatter example of the previous chapter, we would implement CanFormatString for a Person context as follows:

#![allow(unused)]
fn main() {
use core::fmt::{self, Display};

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

#[derive(Debug)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

impl Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.first_name, self.last_name)
    }
}

impl CanFormatString for Person {
    fn format_string(&self) -> String {
        FormatStringWithDisplay::format_string(self)
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };

assert_eq!(person.format_string(), "John Smith");

pub struct FormatStringWithDisplay;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}
}

To recap the previous chapter, we have a consumer trait CanFormatString and a provider trait StringFormatter. There are two example providers that implemenent StringFormatter - FormatStringWithDisplay which formats strings using Display, and FormatStringWithDebug which formats strings using Debug. In addition to that, we implement CanFormatString for the Person context by forwarding the call to FormatStringWithDisplay.

By doing so, we effectively "bind" the StringFormatter provider for the Person context to FormatStringWithDisplay. With that, any time a consumer code calls person.format_string(), it would automatically format the context using Display.

Thanks to the decoupling of providers and consumers, a context like Person can freely choose between multiple providers, and link them with relative ease. Similarly, the provider trait allows multiple context-generic providers such as FormatStringWithDisplay and FormatStringWithDebug to co-exist.

Blanket Consumer Trait Implementation

In the previous section, we manually implemented CanFormatString for Person with an explicit call to FormatStringWithDisplay. Although the implementation is relatively short, it can become tedious if we make heavy use of provider traits, which would require us to repeat the same pattern for every trait.

To simplify this further, we can make use of blanket implementations to automatically delegate the implementation of all consumer traits to one chosen provider. We would define the blanket implementation for CanFormatString as follows:

#![allow(unused)]
fn main() {
pub trait HasComponents {
    type Components;
}

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_string(&self) -> String {
        Context::Components::format_string(self)
    }
}
}

First of all, we define a new HasComponents trait that contains an associated type Components. The Components type would be specified by a context to choose a provider that it would use to forward all implementations of consumer traits. Following that, we add a blanket implementation for CanFormatString, which would be implemented for any Context that implements HasComponents, provided that Context::Components implements StringFormatter<Context>.

To explain in simpler terms - if a context has a provider that implements a provider trait for that context, then the consumer trait for that context is also automatically implemented.

With the new blanket implementation in place, we can now implement HasComponents for the Person context, and it would now help us to implement CanFormatString for free:

#![allow(unused)]
fn main() {
use core::fmt::{self, Display};

#[derive(Debug)]
pub struct Person {
    pub first_name: String,
    pub last_name: String,
}

impl Display for Person {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{} {}", self.first_name, self.last_name)
    }
}

impl HasComponents for Person {
    type Components = FormatStringWithDisplay;
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };

assert_eq!(person.format_string(), "John Smith");

pub trait HasComponents {
    type Components;
}

pub trait CanFormatString {
    fn format_string(&self) -> String;
}

pub trait StringFormatter<Context> {
    fn format_string(context: &Context) -> String;
}

impl<Context> CanFormatString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_string(&self) -> String {
        Context::Components::format_string(self)
    }
}

pub struct FormatStringWithDisplay;

impl<Context> StringFormatter<Context> for FormatStringWithDisplay
where
    Context: Display,
{
    fn format_string(context: &Context) -> String {
        format!("{}", context)
    }
}
}

Compared to before, the implementation of HasComponents is much shorter than implementing CanFormatString directly, since we only need to specify the provider type without any function definition.

At the moment, because the Person context only implements one consumer trait, we can set FormatStringWithDisplay directly as Person::Components. However, if there are other consumer traits that we would like to use with Person, we would need to define Person::Components with a separate provider that implements multiple provider traits. This will be covered in the next chapter, which we would talk about how to link multiple providers of different provider traits together.

Component System

You may have noticed that the trait for specifying the provider for a context is called HasComponents instead of HasProviders. This is to generalize the idea of a pair of consumer trait and provider trait working together, forming a component.

In context-generic programming, we use the term component to refer to a consumer-provider trait pair. The consumer trait and the provider trait are linked together through blanket implementations and traits such as HasComponents. These constructs working together to form the basis for a component system for CGP.

Provider Delegation

In the previous chapter, we learned to make use of the HasComponents trait to define a blanket implementation for a consumer trait like CanFormatString, so that a context would automatically delegate the implementation to a provider trait like StringFormatter. However, because there can only be one Components type defined for HasComponents, this means that the given provider needs to implement all provider traits that we would like to use for the context.

In this chapter, we will learn to combine multiple providers that each implements a distinct provider trait, and turn them into a single provider that implements multiple provider traits.

Implementing Multiple Providers

Consider that instead of just formatting a context as string, we also want to parse the context from string. In CGP, we would define two separate traits to handle the functionalities separately:

#![allow(unused)]
fn main() {
extern crate anyhow;

use anyhow::Error;

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}
}

Similar to the previous chapter, we define CanFormatToString for formatting a context into string, and CanParseFromString for parsing a context from a string. Notice that CanParseFromString also has an additional Sized constraint, as by default the Self type in Rust traits do not implement Sized, to allow traits to be used in dyn trait objects. Compared to before, we also make the methods return a Result to handle errors during formatting and parsing. 1 2

Next, we also define the provider traits as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;

use anyhow::Error;

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}
}

Similar to the previous chapter, we make use of blanket implementations and HasComponents to link the consumer traits CanFormatToString and CanParseFromString with their respective provider traits, StringFormatter and StringParser.

We can then implement context-generic providers for the given provider traits, such as to format and parse the context as JSON if the context implements Serialize and Deserialize:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

use serde::{Serialize, Deserialize};

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)?)
    }
}
}

The provider FormatAsJsonString implements StringFormatter for any Context type that implements Serialize, and uses serde_json::to_string to format the context as JSON. Similarly, the provider ParseFromJsonString implements StringParser for any Context that implements Deserialize, and parse the context from a JSON string.

1

A proper introduction to error handling using CGP will be covered in future chapters. But for now, we will use use anyhow::Error to handle errors in a more naive way.

2

There are more general terms the problem space of formatting and parsing a context, such as serialization or encoding. Instead of strings, a more general solution may use types such as bytes or buffers. Although it is possible to design a generalized solution for encoding in CGP, it would be too much to cover the topic in this chapter alone. As such, we use naive strings in this chapter so that we can focus on first understanding the basic concepts in CGP.

Linking Multiple Providers to a Concrete Context

With the providers implemented, we can now define a concrete context like Person, and link it with the given providers. However, since there are multiple providers, we need to first define an aggregated provider called PersonComponents, which would implement both StringFormatter and StringParser by delegating the call to the actual providers.

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}

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)?)
    }
}

#[derive(Serialize, 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;
}

impl StringFormatter<Person> for PersonComponents {
    fn format_to_string(person: &Person) -> Result<String, Error> {
        FormatAsJsonString::format_to_string(person)
    }
}

impl StringParser<Person> for PersonComponents {
    fn parse_from_string(raw: &str) -> Result<Person, Error> {
        ParseFromJsonString::parse_from_string(raw)
    }
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{"first_name":"John","last_name":"Smith"}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

We first define Person struct with auto-derived implementations of Serialize and Deserialize. We also auto-derive Debug and Eq for use in tests later on.

We then define a dummy struct PersonComponents, which would be used to aggregate the providers for Person. Compared to the previous chapter, we implement HasComponents for Person with PersonComponents as the provider.

We then implement the provider traits StringFormatter and StringParser for PersonComponents, with the actual implementation forwarded to FormatAsJsonString and ParseFromJsonString.

Inside the test that follows, we verify that the wiring indeed automatically implements CanFormatToString and CanParseFromString for Person, with the JSON implementation used.

Blanket Provider Implementation

Although the previous example works, the boilerplate for forwarding multiple implementations by PersonComponents seems a bit tedious and redundant. The main differences between the two implementation boilerplate is that we want to choose FormatAsJsonString as the provider for StringFormatter, and ParseFromJsonString as the provider for StringParser.

Similar to how we can use HasComponents with blanket implementations to link a consumer with a provider, we can reduce the boilerplate required by using similar pattern to link a provider with another provider:

#![allow(unused)]
fn main() {
extern crate anyhow;

use anyhow::Error;

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

pub trait DelegateComponent<Name> {
    type Delegate;
}

pub struct StringFormatterComponent;

pub struct StringParserComponent;

impl<Context, Component> StringFormatter<Context> for Component
where
    Component: DelegateComponent<StringFormatterComponent>,
    Component::Delegate: StringFormatter<Context>,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Component::Delegate::format_to_string(context)
    }
}

impl<Context, Component> StringParser<Context> for Component
where
    Component: DelegateComponent<StringParserComponent>,
    Component::Delegate: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Component::Delegate::parse_from_string(raw)
    }
}
}

The DelegateComponent is similar to the HasComponents trait, but it is intended to be implemented by providers instead of concrete contexts. It also has an extra generic Name type that is used to differentiate which component the provider delegation is intended for.

To make use of the Name parameter, we first need to assign names to the CGP components that we have defined. We first define the dummy struct StringFormatterComponent to be used as the name for StringFormatter, and StringParserComponent to be used as the name for StringParser. In general, we can choose any type as the component name. However by convention, we choose to add a -Component postfix to the name of the provider trait to be used as the name of the component.

We then define a blanket implementation for StringFormatter, which is implemented for a provider type Component with the following conditions: if the provider implements DelegateComponent<StringFormatterComponent>, and if the associated type Delegate also implements StringFormatter<Context>, then Component also implements StringFormatter<Context> by delegating the implementation to Delegate.

Following the same pattern, we also define a blanket implementation for StringParser. The main difference here is that the name StringParserComponent is used as the type argument to DelegateComponent. In other words, different blanket provider implementations make use of different Name types for DelegateComponent, allowing different Delegate to be used depending on the Name.

Using DelegateComponent

It may take a while to fully understand how the blanket implementations with DelegateComponent and HasComponents work. But since the same pattern will be used everywhere, it would hopefully become clear as we see more examples. It would also help to see how the blanket implementation is used, by going back to the example of implementing the concrete context Person.

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}

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)?)
    }
}

pub trait DelegateComponent<Name> {
    type Delegate;
}

pub struct StringFormatterComponent;

pub struct StringParserComponent;

impl<Context, Component> StringFormatter<Context> for Component
where
    Component: DelegateComponent<StringFormatterComponent>,
    Component::Delegate: StringFormatter<Context>,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Component::Delegate::format_to_string(context)
    }
}

impl<Context, Component> StringParser<Context> for Component
where
    Component: DelegateComponent<StringParserComponent>,
    Component::Delegate: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Component::Delegate::parse_from_string(raw)
    }
}
#[derive(Serialize, 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;
}

impl DelegateComponent<StringFormatterComponent> for PersonComponents {
    type Delegate = FormatAsJsonString;
}

impl DelegateComponent<StringParserComponent> for PersonComponents {
    type Delegate = ParseFromJsonString;
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{"first_name":"John","last_name":"Smith"}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

Instead of implementing the provider traits, we now only need to implement DelegateComponent<StringFormatterComponent> and DelegateComponent<StringParserComponent> for PersonComponents. Rust's trait system would then automatically make use of the blanket implementations to implement CanFormatToString and CanParseFromString for Person.

As we will see in the next chapter, we can make use of macros to further simplify the component delegation, making it as simple as one line to implement such delegation.

Switching Provider Implementations

With the given examples, some readers may question why is there a need to define multiple providers for the JSON implementation, when we can just define one provider struct and implement both provider traits for it.

The use of two providers in this chapter is mainly used as demonstration on how to delegate and combine multiple providers. In practice, as the number of CGP components increase, we would also quickly run into the need have multiple provider implementations and choosing between different combination of providers.

Even with the simplified example here, we can demonstrate how a different provider for StringFormatter may be needed. Supposed that we want to format the context as prettified JSON string, we can define a separate provider FormatAsPrettifiedJsonString as follows:

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub struct FormatAsPrettifiedJsonString;

impl<Context> StringFormatter<Context> for FormatAsPrettifiedJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string_pretty(context)?)
    }
}
}

In the StringFormatter implementation for FormatAsPrettifiedJsonString, we use serde_json::to_string_pretty instead of serde_json::to_string to pretty format the JSON string. With CGP, both FormatAsPrettifiedJsonString and FormatAsJsonString can co-exist peacefully, and we can easily choose which provider to use in each concrete context implementation.

#![allow(unused)]
fn main() {
extern crate anyhow;
extern crate serde;
extern crate serde_json;

use anyhow::Error;
use serde::{Serialize, Deserialize};

pub trait HasComponents {
    type Components;
}

pub trait CanFormatToString {
    fn format_to_string(&self) -> Result<String, Error>;
}

pub trait CanParseFromString: Sized {
    fn parse_from_string(raw: &str) -> Result<Self, Error>;
}

pub trait StringFormatter<Context> {
    fn format_to_string(context: &Context) -> Result<String, Error>;
}

pub trait StringParser<Context> {
    fn parse_from_string(raw: &str) -> Result<Context, Error>;
}

impl<Context> CanFormatToString for Context
where
    Context: HasComponents,
    Context::Components: StringFormatter<Context>,
{
    fn format_to_string(&self) -> Result<String, Error> {
        Context::Components::format_to_string(self)
    }
}

impl<Context> CanParseFromString for Context
where
    Context: HasComponents,
    Context::Components: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Context::Components::parse_from_string(raw)
    }
}

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 FormatAsPrettifiedJsonString;

impl<Context> StringFormatter<Context> for FormatAsPrettifiedJsonString
where
    Context: Serialize,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Ok(serde_json::to_string_pretty(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)?)
    }
}

pub trait DelegateComponent<Name> {
    type Delegate;
}

pub struct StringFormatterComponent;

pub struct StringParserComponent;

impl<Context, Component> StringFormatter<Context> for Component
where
    Component: DelegateComponent<StringFormatterComponent>,
    Component::Delegate: StringFormatter<Context>,
{
    fn format_to_string(context: &Context) -> Result<String, Error> {
        Component::Delegate::format_to_string(context)
    }
}

impl<Context, Component> StringParser<Context> for Component
where
    Component: DelegateComponent<StringParserComponent>,
    Component::Delegate: StringParser<Context>,
{
    fn parse_from_string(raw: &str) -> Result<Context, Error> {
        Component::Delegate::parse_from_string(raw)
    }
}
#[derive(Serialize, 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;
}

impl DelegateComponent<StringFormatterComponent> for PersonComponents {
    type Delegate = FormatAsPrettifiedJsonString;
}

impl DelegateComponent<StringParserComponent> for PersonComponents {
    type Delegate = ParseFromJsonString;
}

let person = Person { first_name: "John".into(), last_name: "Smith".into() };
let person_str = r#"{
  "first_name": "John",
  "last_name": "Smith"
}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str,
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

Compared to before, the only line change is to set the Delegate of DelegateComponent<StringFormatterComponent> to FormatAsPrettifiedJsonString instead of FormatAsJsonString. With that, we can now easily choose between whether to pretty print a Person context as JSON.

Beyond having a prettified implementation, it is also easy to think of other kinds of generic implementations, such as using Debug or Display to format strings, or use different encodings such as XML to format the string. With CGP, we can define generalized component interfaces that are applicable to a wide range of implementations. We can then make use of DelegateComponent to easily choose which implementation we want to use for different concrete contexts.

Component Macros

At this point, we have covered all basic building blocks of defining CGP components. In summary, a CGP component is consist of the following building blocks:

  • A consumer trait.
  • A provider trait.
  • A component name type.
  • A blanket implementation of the consumer trait using HasComponents.
  • A blanket implementation of the provider trait using DelegateComponent.

Syntactically, all CGP components follow the same pattern. The pattern is roughly as follows:

// Consumer trait
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

// Provider trait
pub trait ActionPerformer<Context, GenericA, GenericB, ...>
where
    Context: ConstraintA + ConstraintB + ...,
{
    fn perform_action(
        context: &Context,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

// Component name
pub struct ActionPerformerComponent;

// Blanket implementation for consumer trait
impl<Context, GenericA, GenericB, ...>
    CanPerformAction<GenericA, GenericB, ...> for Context
where
    Context: HasComponents + ConstraintA + ConstraintB + ...,
    Context::Components: ActionPerformer<Context>,
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output {
        Context::Components::perform_action(self, arg_a, arg_b, ...)
    }
}

// Blanket implementation for provider trait
impl<Context, Component, GenericA, GenericB, ...>
    ActionPerformer<Context, GenericA, GenericB, ...>
    for Component
where
    Context: ConstraintA + ConstraintB + ...,
    Component: DelegateComponent<ActionPerformerComponent>,
    Component::Delegate: ActionPerformer<Context, GenericA, GenericB, ...>,
{
    fn perform_action(
        context: &Context,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output {
        Component::Delegate::perform_action(context, arg_a, arg_b, ...)
    }
}

derive_component Macro

With the repetitive pattern, it makes sense that we should be able to just define the consumer trait, and make use of Rust macros to generate the remaining code. The author has published the cgp Rust crate that provides the derive_component macro that can be used for this purpose. Using the macro, the same code as above can be significantly simplified to the following:

use cgp::prelude::*;

#[derive_component(ActionPerformerComponent, ActionPerformer<Context>)]
pub trait CanPerformAction<GenericA, GenericB, ...>:
    ConstraintA + ConstraintB + ...
{
    fn perform_action(
        &self,
        arg_a: ArgA,
        arg_b: ArgB,
        ...
    ) -> Output;
}

To use the macro, the bulk import statement use cgp::prelude::* has to be used to bring all CGP constructs into scope. This includes the HasComponents and DelegateComponent traits, which are also provided by the cgp crate.

We then use derive_component as an attribute proc macro, with two arguments given to the macro. The first argument, ActionPerformerComponent, is used to define the name type. The second argument, ActionPerformer<Context>, is used as the name for the provider trait, as well as the generic type name for the context.

delegate_components Macro

In addition to the derive_component macro, cgp also provides the delegate_components! macro that can be used to automatically implement DelegateComponent for a provider type. The syntax is roughly as follows:

use cgp::prelude::*;

pub struct TargetProvider;

delegate_components! {
    TargetProvider {
        ComponentA: ProviderA,
        ComponentB: ProviderB,
        [
            ComponentC1,
            ComponentC2,
            ...
        ]: ProviderC,
    }
}

The above code would be desugared into the following:

impl DelegateComponent<ComponentA> for TargetProvider {
    type Delegate = ProviderA;
}

impl DelegateComponent<ComponentB> for TargetProvider {
    type Delegate = ProviderB;
}

impl DelegateComponent<ComponentC1> for TargetProvider {
    type Delegate = ProviderC;
}

impl DelegateComponent<ComponentC2> for TargetProvider {
    type Delegate = ProviderC;
}

The delegate_components! macro accepts an argument to an existing type, TargetProvider, which is expected to be defined outside of the macro. It is followed by an open brace, and contain entries that look like key-value pairs. For a key-value pair ComponentA: ProviderA, the type ComponentA is used as the component name, and ProviderA refers to the provider implementation. When multiple keys map to the same value, i.e. multiple components are delegated to the same provider implementation, the array syntax can be used to further simplify the mapping.

Example Use

To illustrate how derive_component and delegate_components can be used, we revisit the code for CanFormatToString, CanParseFromString, and PersonContext from the previous chapter, and look at how the macros can simplify the same code.

Following is the full code after simplification using cgp:

#![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};

// Component definitions

#[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>;
}

// Provider implementations

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)?)
    }
}

// Concrete context and wiring

#[derive(Serialize, 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() };
let person_str = r#"{"first_name":"John","last_name":"Smith"}"#;

assert_eq!(
    person.format_to_string().unwrap(),
    person_str
);

assert_eq!(
    Person::parse_from_string(person_str).unwrap(),
    person
);
}

As we can see, the new code is significantly simpler and more readable than before. Using derive_component, we no longer need to explicitly define the provider traits StringFormatter and StringParser, and the blanket implementations can be omitted. We also make use of delegate_components! on PersonComponents to delegate StringFormatterComponent to FormatAsJsonString, and StringParserComponent to ParseFromJsonString.

CGP Macros as Language Extension

The use of cgp crate with its macros is essential in enabling the full power of context-generic programming in Rust. Without it, programming with CGP would become too verbose and full of boilerplate code.

On the other hand, the use of cgp macros makes CGP code look much more like programming in a domain-specific language (DSL) than in regular Rust. In fact, one could argue that CGP acts as a language extension to the base language Rust, and almost turn into its own programming language.

In a way, implementing CGP in Rust is slightly similar to implementing OOP in C. We could think of context-generic programming being as foundational as object-oriented programming, and may be integrated as a core language feature in future programming languages.

Perhaps one day, there might be an equivalent of C++ to replace CGP-on-Rust. Or perhaps more ideally, the core constructs of CGP would one day directly supported as a core language feature in Rust. But until that happens, the cgp crate serves as an experimental ground on how context-generic programming can be done in Rust, and how it can help build better Rust applications.

In the chapters that follow, we will make heavy use of cgp and its macros to dive further into the world of context-generic programming.

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.