Function overloading without traits?

I have a procedure that consists of several steps. The procedure has few modes of operation, and I'd like to have hand-optimized functions for each step for each mode of operation.

If I use enum for modes of operation, I find the code a bit repetitive:

fn animal_orchestra(loudness: LoudnessEnum) {
    match loud {
        Loud => quack_loud(quack_args), 
        Quiet => quack_quiet(quack_args), 
    }
    pause();
    match loud {
        Loud => bark_loud(), 
        Quiet => bark_quiet(), 
    }
}

I know I could create a trait and newtypes for each mode and then parametrize the function on that:

fn animal_orchestra<L: LoudnessTrait>(loudness: L) {
    loudness.quack(quack_args), 
    pause();
    loudness.bark(), 
}

but that requires creation of a sort-of kitchen-sink trait, and implementing unrelated methods on that trait. That looks odd. Is there a better way to implement that?

Could you flip it around and do something like:

    trait Noise {
        fn loud(&self);
        fn quiet(&self);
    }
    
    enum Loudness {
        Loud,
        Quiet
    }
    
    impl Loudness {
        fn make<T : Noise>(&self, noise : &T) {
            match *self {
                Loudness::Loud => noise.loud(),
                Loudness::Quiet => noise.quiet()
            }
        }
    }
    
    fn animal_orchestra<T : Noise>(loudness : Loudness, noise: [T;2]) {
        loudness.make(&noise[0]);
        pause();
        loudness.make(&noise[1]);
    }

The downside I guess would you would have to have a list of noises....

By that you mean the argument to the function animal_orchestra?
Because if there are always two parameters you could simply write them out as two of them and otherwise it would make perfect sense to have them being passed as a slice (&[T]) so you can iterate over them which makes it way more flexible/expandable.

Edit: example:

fn animal_orchestra<T : Noise>(loudness : Loudness, first: &T, second: &T) {
    loudness.make(first);
    pause();
    loudness.make(second);
}
// or when using a slice
fn animal_orchestra<T : Noise>(loudness : Loudness, noises: &[T]) {
    for noise in noises {
        loudness.make(noise);
        pause();
    }
}
// if you don't want a pause at the end
fn animal_orchestra<T : Noise>(loudness : Loudness, noises: &[T]) {
    loudness.make(&noises[0]);
    for noise in &noises[1..] {
        pause();
        loudness.make(noise);
    }
}

The other advantage would be that even a third party could impl the Noise and therefore supply additional noises which is great for expanding functionality.

Edit: example:

// you could easily implement for your types of course
struct Quack;

impl Noise for Quack {
    fn loud(&self) {
        // …
    }
    fn quiet(&self) {
        // …
    }
}
// and if you would expose these types somehow via a library (that could have `--bin`ary target(s))
// you could also extend this inside other crates
extern crate animal_orchestra;
use animal_orchestra::{Noise,Loudness,Quack,animal_orchestra as play};

struct Meow;
impl Noise for Meow {
    fn loud(&self) {
        // …
    }
    fn quiet(&self) {
        // …
    }
}

fn main() {
    let v = vec![];
    // fill `v` with Meow and Quack's
    play(Loudness::Load,&v);
}

Unfortunately neither of these options is satisfactory, because it breaks encapsulation and requires the caller to specify what the function will be doing. I need to keep all the "noises" a hidden implementation detail. My real code is not that simple and can't be replaced with a loop, and each step takes different arguments, some steps are optional, etc.

Okay, so, going back to your original code:

Can't you simply pass the enum variant to a e.g. quack function? You could then put each of these functions into a submodule (quack::quack, meow::meow) that only exports that single function which itself does only match on the variant and calls another unexported function (meow::meow_load or meow::meow_quiet) that is only visible within that very module?

Edit: full example:

fn main()
{
    animal_orchestra(Loudness::Loud);
}

#[derive(Copy,Clone)]
pub enum Loudness
{
    Loud,
    Quiet,
}

fn animal_orchestra(loudness: Loudness)
{
    use quack::quack;
    use meow::meow;
    
    quack(loudness);
    pause();
    meow(loudness /* arguments */);
}

fn pause()
{
}

// this should be done using cargo (put inside another file)
mod quack
{
    use super::Loudness;
    pub fn quack(loud: Loudness)
    {
        match loud
        {
            Loudness::Loud => quack_loud(),
            Loudness::Quiet => quack_quiet(),
        }
    }
    
    fn quack_loud(){}
    fn quack_quiet(){}
}

// this should be done using cargo (put inside another file)
mod meow
{
    use super::Loudness;
    pub fn meow(loud: Loudness /* arguments */)
    {
        match loud
        {
            Loudness::Loud => meow_loud(/* arguments */),
            Loudness::Quiet => meow_quiet(/* arguments */),
        }
    }
    
    fn meow_loud(/* arguments */){}
    fn meow_quiet(/* arguments */){}
}

It doesn't reduce number of matches, just moves them elsewhere.

I suppose I could use such approach as a facade to hide weirdness of the Trait version:

fn bark<L>(loudness: L) {
    loudness.bark();
}

You could use a macro for generating the necessary modules though, meaning you could use exactly the code from this gist.

Edit: this is the first macro I've ever written, there might be some way to annotate which block is the loud one and which is the quiet one, also there should be some way to specify arguments, but I have no idea how to do that.

You should probably add some type-restrictions to that: