-
Notifications
You must be signed in to change notification settings - Fork 8
Description
Problem Statement
A critical challenge for CGP to gain widespread adoption is that even the most basic concept of blanket implementation requires Rust developers to learn advanced trait system concepts.
For example, consider the minimal context-generic function for calculating the area of rectangle:
#[cgp_auto_getter]
pub trait HasRectangleFields {
fn width(&self) -> f64,
fn height(&self) -> f64,
}
pub trait RectangleArea {
fn rectangle_area(&self) -> f64;
}
impl<Context> RectangleArea for Context
where
Context: HasRectangleFields,
{
fn rectangle_area(&self) -> f64 {
self.width() * self.height()
}
}There are still way too much boilerplate and complexity for a beginner to understand. The user has to define a getter trait for the fields, then define another trait for the interface, and finally implement the area trait for any context that implements the fields trait.
On the other hand, Rust developers are intimately familiar with the dependency injection concepts from frameworks like Axum and Bevy. In these frameworks, the user only needs to write plain Rust functions, while the framework implements the heavy machinery to supply the dependencies to the functions.
Introducing #[cgp_fn]
We will implement a new #[cgp_fn] proc macro that allows Rust developers to make use of dependency injection from a generic context by simply writing a plain Rust function with some additional annotations. For example:
#[cgp_fn]
pub fn rectangle_area(
#[implicit] width: f64,
#[implicit] height: f64,
) -> f64 {
width * height
}The #[cgp_fn] would desugar the function to something like follows:
pub trait RectangleArea {
fn rectangle_area(&self) -> f64;
}
impl<Context> RectangleArea for Context
where
Context: HasField<Symbol!("width"), Value = f64>
+ HasField<Symbol!("height", Value = f64)>,
{
fn rectangle_area(&self) -> f64 {
rectangle_area(
context.get_field(PhantomData::<Symbol!("width")>),
context.get_field(PhantomData::<Symbol!("height")>),
)
}
}
pub fn rectangle_area(
width: f64,
height: f64,
) -> f64 {
width * height
}This would significantly reduce the friction for Rust developers to write context-generic code. #[cgp_fn] essentially adds capability that looks like implicit parameters to Rust. The method can easily be called on any context type that contains the satisfied fields, such as:
#[derive(HasField)]
pub struct Rectangle {
pub width: f64,
pub height: f64,
}
#[test]
fn test() {
let rectangle = Rectangle {
width: 2,
height: 6,
};
assert_eq!(rectangle.rectangle_area(), 6);
}Calling other context-generic functions with #[uses]
We can also add additional attributes to allow context-generic functions to call each others much more easily. Such as:
#[cgp_fn]
#[uses(RectangleArea)]
pub fn scaled_rectangle_area(
&self,
#[implicit] scale_factor: f64,
) -> f64 {
self.rectangle_area() * scale_factor * scale_factor
}This would be desugared to:
pub trait ScaledRectangleArea {
fn scaled_rectangle_area(&self) -> f64;
}
impl<Context> ScaledRectangleArea for Context
where
Context: HasField<Symbol!("scale_factor"), Value = f64> + RectangleArea,
{
fn scaled_rectangle_area(&self) -> f64 {
scaled_rectangle_area(
self,
self.get_field(PhantomData::<Symbol!("scale_factor")>),
)
}
}
pub fn scaled_rectangle_area<Context>(
context: &Context,
scale_factor: f64
) -> f64
where
Context: RectangleArea,
{
context.rectangle_area() * scale_factor * scale_factor
}As we can see, the calling of another context-generic function is almost as natural as the plain version. The main boilerplate introduced is mainly the #[uses(RectangleArea)], which would implicitly add it as a trait bound on the context in the where clause.
Including abstract types with #[use_type]
We can also extend #[cgp_fn] to simplify the use of abstract types by introducing a #[use_type] attribute, such as:
#[cgp_fn]
#[use_type(HasScalarType::Scalar)]
pub fn rectangle_area(
#[implicit] width: Scalar,
#[implicit] height: Scalar,
) -> Scalar {
width * height
}This would be desugared to:
pub trait RectangleArea: HasScalarType {
fn rectangle_area(&self) -> Self::Scalar;
}
impl<Context> RectangleArea for Context
where
Context: HasField<Symbol!("width"), Value = Self::Scalar>
+ HasField<Symbol!("height"), Value = Self::Scalar>
+ HasScalarType,
{
fn rectangle_area(&self) -> Self::Scalar {
rectangle_area::<Context>(
context.get_field(PhantomData::<Symbol!("width")>),
context.get_field(PhantomData::<Symbol!("height")>),
)
}
}
pub fn rectangle_area<Context>(
width: Context::Scalar,
height: Context::Scalar,
) -> Context::Scalar
where
Context: HasScalarType,
{
width * height
}This allows us to skip even the Self:: prefix on abstract types, and allows us to write abstract types as if they are concrete types.
This would especially be useful for abstract types like HasErrorType::Error. Without this, it would be very challenging to justify a whole application to only use abstract error handling without using other context-generic features, because it requires too much boilerplate.