Rust calling default implementation of function in specialized version
Asked Answered
O

2

9

I have a trait in Rust that offers a few default implementations for its functions.

trait MyTrait {
    fn do_something(&self);
    fn say_hello(&self) {
        println!("Hello I am default");
    }
}

Some implementors extend this trait and use the provided defaults

struct MyNormalImplementor {}

impl MyTrait for MyNormalImplementor {
    fn do_something(&self) {
        // self.doing_some_normal_stuff();
    }
}

Now I would like to have an implementor that extends the behavior of the trait, but still uses the default implementation sometimes. Of course the default implementationis more complex and I want to follow the DRY principle.

struct MySpecializedImplementor(bool)

impl MyTrait for MySpecializedImplementor {
    fn do_something(&self) {
        // self.doing_some_wild_stuff();
    }
    fn say_hello(&self) {
        if self.0 {
            println!("hey, I am special");
        } else {
           MyTrait::say_hello(self);
        }
    }
}

Here MyTrait::say_hello(self); immediately calls the specialized function in an endless loop. I did not find any way to qualify the function call so that the default implementation in MyTrait is called instead. Is there any way to achieve that, or do I have to create a proxy function (that will be in the public interface of my trait as well) for that case?

Outgrow answered 6/2, 2021 at 13:2 Comment(0)
T
11

Free-standing generic function

Defer the default implementation to a free-standing generic function:

fn say_hello<T: Trait + ?Sized>(t: &T) {
    println!("Hello I am default")
}

trait Trait {
    fn say_hello(&self) {
        say_hello(self);
    }
}

struct Normal;

impl Trait for Normal {}

struct Special(bool);

impl Trait for Special {
    fn say_hello(&self) {
        if self.0 {
            println!("Hey I am special")
        } else {
            say_hello(self)
        }
    }
}

fn main() {
    let normal = Normal;
    normal.say_hello(); // default

    let special = Special(false);
    special.say_hello(); // default

    let special = Special(true);
    special.say_hello(); // special
}

playground

Two default trait methods

Another approach could be defining two trait methods, one as a default implementation and the other which defers to the default implementation unless it is overwritten:

trait Trait {
    fn say_hello_default(&self) {
        println!("Hello I am default");
    }
    fn say_hello(&self) {
        self.say_hello_default();
    }
}

struct Normal;

impl Trait for Normal {}

struct Special(bool);

impl Trait for Special {
    fn say_hello(&self) {
        if self.0 {
            println!("Hey I am special");
        } else {
            self.say_hello_default();
        }
    }
}

fn main() {
    let normal = Normal;
    normal.say_hello(); // default

    let special = Special(false);
    special.say_hello(); // default
    
    let special = Special(true);
    special.say_hello(); // special
}

playground


Default associated consts

Although this is a tad more clunky, if the difference between the default and specialized implementations be reduced down to const values then you can use default associated const trait items for your trait:

trait Trait {
    const MSG: &'static str = "Hello I am default";
    fn say_hello(&self) {
        println!("{}", Self::MSG);
    }
}

struct Normal;

impl Trait for Normal {}

struct Special(bool);

impl Trait for Special {
    const MSG: &'static str = "Hey I am special";
    fn say_hello(&self) {
        let msg = if self.0 {
            Self::MSG
        } else {
            <Normal as Trait>::MSG
        };
        println!("{}", msg);
    }
}

fn main() {
    let normal = Normal;
    normal.say_hello(); // default

    let special = Special(false);
    special.say_hello(); // default

    let special = Special(true);
    special.say_hello(); // special
}

playground


Call Default implementation via AsRef

If the only thing that differentiates Special from Normal is a few extra fields, and the Special type can otherwise function as a Normal then you may want to implement AsRef<Normal> for Special and call the default implementation that way:

trait Trait {
    fn say_hello(&self) {
        println!("Hello I am default");
    }
}

struct Normal;

impl Trait for Normal {}

struct Special(bool);

impl AsRef<Normal> for Special {
    fn as_ref(&self) -> &Normal {
        &Normal
    }
}

impl Trait for Special {
    fn say_hello(&self) {
        if self.0 {
            println!("Hey I am special");
        } else {
            <Normal as Trait>::say_hello(self.as_ref());
        }
    }
}

fn main() {
    let normal = Normal;
    normal.say_hello(); // default

    let special = Special(false);
    special.say_hello(); // default

    let special = Special(true);
    special.say_hello(); // special
}

playground


Default macro implementation

As usual, if all else fails, the most brute force way to make your code DRY is to use macros:

macro_rules! default_hello {
    () => {
        println!("Hello I am default");
    }
}

trait Trait {
    fn say_hello(&self) {
        default_hello!();
    }
}

struct Normal;

impl Trait for Normal {}

struct Special(bool);

impl Trait for Special {
    fn say_hello(&self) {
        if self.0 {
            println!("Hey I am special");
        } else {
            default_hello!();
        }
    }
}

fn main() {
    let normal = Normal;
    normal.say_hello(); // default

    let special = Special(false);
    special.say_hello(); // default

    let special = Special(true);
    special.say_hello(); // special
}

playground

Tacmahack answered 6/2, 2021 at 13:52 Comment(2)
For the last suggestion, using macros seems a bit like overkill, as you could just as well have used just a regular function, right? The syn::Visit visitor trait, for instance, does exactly this with just a bunch of free-standing functions and the default implementation of the trait methods just call these functions.Ketonuria
Thank you @pretzelhammer. Of course I hoped that there might be a better solution for it, similar like what you can do when competing methods from different traits can be explicitly chosen. But I am sure I will make my programm run with one solution you suggested.Gilgamesh
K
3

The syn::Visit trait, for example, has similar needs and does this: for each trait method, there's a corresponding free-standing function, and all the default implementation does is call the corresponding free-standing function. If a trait implementation needs to do something else and delegate to the default behavior, it just does whatever it needs to do and calls that free-standing function itself.

For your example, it could look something like this:

// default implementation
fn say_hello<T: ?Sized + MyTrait>(t: &T) {
    println!("Hello I am default");
}

trait MyTrait {
    fn do_something(&self);
    fn say_hello(&self) {
        // use default behavior
        say_hello(self);
    }
}

struct MySpecializedImplementor(bool)

impl MyTrait for MySpecializedImplementor {
    fn do_something(&self) {
        // self.doing_some_wild_stuff();
    }

    fn say_hello(&self) {
        if self.0 {
            println!("hey, I am special");
        } else {
            // use default behavior
            say_hello(self);
        }
    }
}
Ketonuria answered 6/2, 2021 at 14:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.