0

I'm just starting in the Rust (come from python and C#) world and I know this question has been asked many times, but I'm struggling to understand the answers as they are very generic (Foo, Bar, Baz). I'm not even sure if what I need is achievable in Rust.

I need a class to inherit a method from its parent and grand-parent as well and force it to implement functions defined in its parent.

Case:

As you can see in the Code section bellow, Days30BondCounter and Days30ECounter structs return the day_count with the same expresion. All the difference is in how d1 and d2 is calculated.

My Idea is to create a Days30 abstract class that inherits from DayCounter but that requires implementing get_d1 and get_d2 functions. So that Days30 looks something like

trait Days30 : DayCounter {
    fn get_d1(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32;
    fn get_d2(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32;

    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64 {
        let d1 = self.get_d1(start_date, end_date);
        let d2 = self.get_d2(start_date, end_date);
        (360 * (end_date.year() - start_date.year()) + 30 * ((end_date.month() - start_date.month()) as i32) + d2 - d1) as i64
    }
}

struct Days30BondCounter;
impl Days30 for Days30BondCounter { // this fails
    fn get_d1(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32 {
        1 // d1 logic
    }
    fn get_d2(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32 {
        2 // d2 logic
    }
}

Code:

use chrono::{Datelike, NaiveDate, TimeDelta};
use std::cmp::min;

trait DayCounter { // Base Class
    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64;

    fn day_count_vector(&self, start_date: NaiveDate, end_dates: &Vec<NaiveDate>) -> Vec<i64> {
        let mut result: Vec<i64> = Vec::with_capacity(end_dates.len()); // Replace with VLA
        for end_date in end_dates {
            result.push(self.day_count(start_date, *end_date));
        }
        result
    }

}

struct ActualCounter; // Simple child
impl DayCounter for ActualCounter {
    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64 {
        let duration: TimeDelta = end_date.signed_duration_since(start_date);
        duration.num_days()    
    }
}

struct Days30BondCounter; // This is currently a Child. I'd like it to inherit from Days30 which inherits from DayCounter
impl DayCounter for Days30BondCounter {
    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64 {
        let mut d1: i32 = start_date.day() as i32;
        d1 = min(d1, 30);
        let mut d2: i32 = end_date.day() as i32;
        if d1 > 29 {
            d2 = min(d2, 30);
        }

        (360 * (end_date.year() - start_date.year()) + 30 * ((end_date.month() - start_date.month()) as i32) + d2 - d1) as i64
    }
}

struct Days30ECounter; // This is currently a Child. I'd like it to inherit from Days30 which inherits from DayCounter
impl DayCounter for Days30ECounter {
    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64 {
        let d1: i32 = min(start_date.day(), 30) as i32;
        let d2: i32 = min(end_date.day(), 30) as i32;
        
        (360 * (end_date.year() - start_date.year()) + 30 * ((end_date.month() - start_date.month()) as i32) + d2 - d1) as i64
    }
}

Extra Info:

  • DayCounter is like the base class. Any implementation must define a way to count days between 2 dates in day_count method.

  • get_d1 and get_d2 are Days30 only methods (non useful in ActualCounter implementation). Its results give d1 and d2 to be used in Days30 implementation of day_count

6
  • 7
    "a Days30 abstract class that inherits from DayCounter but that requires implementing get_d1 and get_d2 functions" - but Rust doesn't have classes, nor "inheritance" - you need to change your way of thinking, instead of trying to code C#/Java in Rust.
    – Dai
    Commented Jun 22 at 2:14
  • Sounds like you should encapsulate common logic into a function.
    – kmdreko
    Commented Jun 22 at 3:03
  • @kmdreko But that would require to call the function in each implementation And also have to implement day_count instead of just implementing get_d1 and get_d2. Commented Jun 22 at 3:35
  • @Dai How do you deal with this in Rust then? I have like 4 other implementations for Day30 like "classes". Repeating code doesn't sound like a good idea. Commented Jun 22 at 3:39
  • 2
    Instead of defining day_count on Days30, which is unrelated to day_count on DayCounter try implementing it. Generically, i.e. impl<T: Days30> DayCounter for T {...}.
    – freakish
    Commented Jun 22 at 4:19

1 Answer 1

0

I'll start by requesting everyone read this article. And some C2 articles too. (i.e. anyone who thinks "inheritance" (without further qualification) is a solution - or even the default solution - to a delegation problem - probably hasn't used inheritance enough in their life such that they finally grok that it isn't a good solution 😺 )


With that out of the way, my interpretation of your mission statement: "My idea is to create..." is this C#:

abstract class DayCounter
{
    public virtual Int64 DayCount()
    {
        // ...
    }
}

// 

sealed class ActualCounter : DayCounter
{
}

//

abstract class Days30 : DayCounter
{
    public override Int64 DayCount()
    {
        // do stuff with `this.GetD1()` and `this.GetD2()`.
    }

    protected abstract Int32 GetD1();
    protected abstract Int32 GetD2();
}

sealed class Days30BondCounter : Days30
{
    protected override Int32 GetD1() { ... }
    protected override Int32 GetD2() { ... }
}

sealed class Days30ECounter : Days30
{
    protected override Int32 GetD1() { ... }
    protected override Int32 GetD2() { ... }
}

...and I'll say this already has a code-smell. IMO, the GetD1 and GetD2 methods in Days30 are unnecessarily exposed private implementation concern of Days30; if Days30 wants to delegate those methods out that's fine, but using inheritance here (with protected abstract) methods seems like a bad idea to me; chiefly, any subclassing of Days30 then introduces more implications compared to if class Days30 just had its own service-type interface for those delegated methods.

Whereas this (below) is how things look if we introduce interface IDays30Backend for delegation - note how Days30 is now no-longer abstract, and is also to provide its own default/baseline implementation of IDays30Backend if its ctor call-site fails to provide one (SomeDefaultDays30BackendImpl).

sealed class Days30 : DayCounter
{
    private readonly IDays30Backend backend;

    public Days30( IDays30Backend? backend )
    {
        this.backend = backend ?? new SomeDefaultDays30BackendImpl();
    }
}

interface IDays30Backend
{
    Int32 GetD1();
    Int32 GetD2();
}

sealed class Days30BondCounter : IDays30Backend
{
    public Int32 GetD1() { ... }
    public Int32 GetD2() { ... }
}

sealed class Days30ECounter : IDays30Backend
{
    public Int32 GetD1() { ... }
    public Int32 GetD2() { ... }
}

...and if you think about it, the same can be said about DayCounter; so without getting into too much detail, DayCounter could be a C# interface type too (don't forget that C# supports default-implementations of interface members now):

interface IDayCounter
{
    virtual Int64 DayCount()
    {
        // (default impl)
    }
}

sealed class Days30 : IDayCounter
{
    // etc
}

sealed class ActualCounter : IDayCounter
{
    // etc
}

...and notice how suddenly we're not using inheritance at all anymore!


...so if we take the above re-composition in C# it becomes easier to translate over to Rust:

use chrono::{Datelike, NaiveDate, TimeDelta, Utc};
use std::cmp::min;

//

trait DayCounter {

    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64;

    fn day_count_vector(&self, start_date: NaiveDate, end_dates: &Vec<NaiveDate>) -> Vec<i64> {
        let mut result: Vec<i64> = Vec::with_capacity(end_dates.len()); // Replace with VLA
        for end_date in end_dates {
            result.push(self.day_count(start_date, *end_date));
        }
        result
    }
}

////////////////////
// ActualCounter:

struct ActualCounter {
    
}

impl DayCounter for ActualCounter {
    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64 {
        let duration: TimeDelta = end_date.signed_duration_since(start_date);
        duration.num_days()    
    }
}

////////////////////
// Days30Counter (nee `Days30`), with `Days30Backend`

struct Days30Counter {
//  backend: dyn Days30Backend
    backend: Box<dyn Days30Backend>
}

impl DayCounter for Days30Counter {
    
    fn day_count(&self, start_date: NaiveDate, end_date: NaiveDate) -> i64 {
        /*
        let mut d1: i32 = start_date.day() as i32;
        d1 = min(d1, 30);
        let mut d2: i32 = end_date.day() as i32;
        if d1 > 29 {
            d2 = min(d2, 30);
        }
        */
        
        let d1 = self.backend.get_d1(start_date, end_date);
        let d2 = self.backend.get_d2(start_date, end_date);

        (360 * (end_date.year() - start_date.year()) + 30 * ((end_date.month() - start_date.month()) as i32) + d2 - d1) as i64
    }
}

trait Days30Backend {
    
    fn get_d1(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32;
    fn get_d2(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32;
}

//

struct Days30BondCounter {
    
}

impl Days30Backend for Days30BondCounter {
    
    fn get_d1(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32 {
        1 // d1 logic
    }
    fn get_d2(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32 {
        2 // d2 logic
    }
}

//

struct Days30ECounter {
    
}

impl Days30Backend for Days30ECounter {
    
    fn get_d1(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32 {
        let mut d1: i32 = start_date.day() as i32;
        d1 = min(d1, 30);
        return d1;
    }
    fn get_d2(&self, start_date: NaiveDate, end_date: NaiveDate) -> i32 {
        let d1 = self.get_d1(start_date, end_date);
        let mut d2: i32 = end_date.day() as i32;
        if d1 > 29 {
            d2 = min(d2, 30);
        }
        return d2;
    }
}

//

fn main() {

    println!("Hello, world!");
    
    let startDt: NaiveDate = Utc::now().date_naive();
    let endDt  : NaiveDate = Utc::now().date_naive();
    
    let dayCounterImpl1 = ActualCounter{};
    
    let dayCounterImpl2 = Days30Counter{
        backend: Box::new(Days30BondCounter{})
    };

    let dayCounterImpl2 = Days30Counter{
        backend: Box::new(Days30ECounter {})
    };
    
    //
    
    let result1 = dayCounterImpl1.day_count( /*startDate:*/ startDt, /*endDate:*/ endDt );
    let result2 = dayCounterImpl2.day_count( /*startDate:*/ startDt, /*endDate:*/ endDt );
    let result3 = dayCounterImpl3.day_count( /*startDate:*/ startDt, /*endDate:*/ endDt );

    println!( "result1: {}", result1 ); // 0
    println!( "result2: {}", result2 ); // 1
    println!( "result3: {}", result2 ); // 1
}

...and it compiles for me in the playground (thought prints zero for output).

(And yes, this really is the first compilable Rust program I've ever written).

Not the answer you're looking for? Browse other questions tagged or ask your own question.