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.