Crate hereditary

source ·
Expand description

Hereditary

Procedural macros for emulating OOP Inheritance in Rust, by extending the trait functionality in structs based on their composition (as they contain instances of other types that implement the desired functionality).

Hereditary generates the boilerplate of trait implementations for the composited struct, by Forwarding the required methods that wrap the functionality already implemented in the contained instances referenced by its struct fields.

Currently, Hereditarysupport 2 kinds of delegation:

  • Partial Delegation: By using the decorator attribute #[forward_trait(submember)] on trait implementations.
  • Full Delegation: By applying #[derive(Forwarding)] on the composited struct, it derives the trait implementation on the struct field (designated by attribute #[forward_derive(Trait)]).

For creating the trait wrappers on subcomponent instances, it is necessary to generate the trait information with the macro attribute trait_info.

The process of incorporating reusable components by type composition, is performed in these three steps:

1. Trait Declaration

Before extending the functionality of traits in submembers, it’s required to declare the macros that create the trait information syntax; such compile time information will be consumed by forwarding macros in later stages. It can be done by just inserting the #[trait_info] attribute on top of trait declartions:

mod animal
{ 
    #[hereditary::trait_info]
    pub trait Cannis {
        fn bark(&self)-> String;
        fn sniff(&self)-> bool;
        fn roam(&mut self, distance:f64) -> f64;
    }
    // What `trait_info` does is declaring a macro with 'TraitInfo_' prefix, that injects 
    // the trait syntax structure as other forwarding macros would consume 
    // that compile time information by invoking the corresponging 'TraitInfo_' macros.
    // The resulting macro will be something like TraitInfo_Cannis(<inner params>)
 
    #[hereditary::trait_info]
    pub trait Bird {
        fn sing(&self) -> String;
        fn fly(&mut self, elevation:f64) -> f64;
    }
}

2. Compoment implementation

Create the basic components that would be reused in composite structs. Such components provides a full implementation of the previously declared traits.

struct Bulldog { position:f64 }
 
impl animal::Cannis for Bulldog {
    fn bark(&self)-> String {
        "Guau!".into()
    }
 
    fn sniff(&self)-> bool {true}
 
    fn roam(&mut self, distance:f64) -> f64 {
        self.position += distance;
        self.position
    }
}
 
// Bird implementation
struct Seagull {  elevation:f64 }
 
impl animal::Bird for Seagull 
{
    fn sing(&self) -> String {  "EEEYA!".into()  }
    fn fly(&mut self, elevation:f64) -> f64 {
        self.elevation += elevation;
        self.elevation
    }
}
 

3. Forwarding Traits in Composition.

By applying the procedural derive Forwarding macro (or the equivalent attribute macro forward_trait on partial trait implementations), composite structs will obtain a trait adaptation by forwarding methods related to their subompoments.

// Heritance for an hybrid animal
#[derive(hereditary::Forwarding)]
struct KimeraSphinx 
{
    // notice that it needs referencing the trait from the animal module path
    #[forward_derive(animal::Cannis)] // full implementation of Cannis
    dogpart:Bulldog,
    birdpart:Seagull
}
 
// Sometimes a new custom behavior is needed.
// By combining new methods with existing functionality inherited from Bird
#[hereditary::forward_trait(birdpart)]
impl animal::Bird for KimeraSphinx
{
    fn sing(&self) -> String
    {
        use crate::animal::Cannis; // have to import the trait here
        // because is a dog, it barks
        self.dogpart.bark()
    }
}
 
fn main() {
    // Have to import the animal traits for accessing their methods here
    use crate::animal::Bird;
    use crate::animal::Cannis;
 
    // Instance kimera
    let mut kimera = KimeraSphinx::new();
    // A dogs that flies.
    assert_eq!(kimera.fly(50f64), 50f64);
    assert_eq!(kimera.bark(), kimera.sing()); // It barks instead of singing
    assert_eq!(kimera.sniff(), true);
}

Features

  • Brings subtype polymorphism on composite structs with just one instruction, vesting the new type with the same interface as its components.
  • Re-use fields/method implementations from other types as subcomponents, without needing to repeately write wrapping code that forwards the methods of those subcomponents.
  • Hereditary tools are essentially zero-cost abstractions. They doesn’t require runtime structures for holding trait type information. All the work it’s done by macros and code generation.
  • Embrace the New Type pattern effectively, but without the previous awkward issues of having to re-implement the inner-type interfaces for the new-type. By using this technique Rust programmers avoid the problems of incorporating new behaviour of existing foreign types, bypassing the Orphan Rule for traits.

Limitations

  • Because of the heavily usage of macros, code made with Hereditary would incurr in longer compilation processes.
  • Sometimes, the traits information cannot be referenced by external modules, because trait_info generated macros aren’t imported automatically as same as their corresponding traits. That’s why they need to be referenced with the full path in the forwarding attributes (animal::Bird), instead of just Bird. This is a known issue related with declarative macros and the scope rules for their visibibility, as they have special needs when exporting them as module symbols.

Attribute Macros

  • Attribute Macro that forwards the unimplemented trait methods on a trait implementation item block, by wrapping instanced trait methods already implemented in the designated struct field.
  • Generates trait information syntax that can be injected as a macro invoke.

Derive Macros

  • Derive procedural macro for generating wrapping trait methods on struct members (designated by the #[forward_derive] attribute) as they bring the required implementation of those traits.