Conciseness: enum vs tuple

I currently have a bunch of functions that do stuff and return nothing. Each function may run f_foo, f_bar, both, or neither before returning.

I'm thinking of moving those calls to f_foo and f_bar outside the functions and changing the return type to indicate whether or not to call f_foo or f_bar, both, or neither.

So far I've identified two possible ways to do this, using either an enum or a tuple:

use Thing::{*};

fn main() {

//method one
    let x = f_1();
    match x {
        Foo(foo) => f_foo(foo),
        Bar(bar) => f_bar(bar),
        FooBar(foo,bar) => {f_foo(foo);f_bar(bar);},
        Nothing => ()
    }
    
//method two
    let (foo, bar) = f_2();
    if let Some(foo) = foo {
        f_foo(foo);
    }
    if let Some(bar) = bar {
        f_bar(bar);
    }
}

fn f_1()-> Thing{
    FooBar(5, false)
}

fn f_2() -> (Option<u16>, Option<bool>){
    (Some(5), Some(false))
}

enum Thing {
    Foo(u16),
    Bar(bool),
    FooBar(u16, bool),
    Nothing,
}

fn f_foo(foo: u16){
    println!("{}", foo);
}

fn f_bar(bar: bool){
    println!("{}", bar);
}

The enum is definitely cleaner when creating the return value - less clutter from Some() everywhere, which is important to me because it is used in a lot of places.
However I find the if let statements that work with the tuple to be cleaner than the match statement with the enum.
Is there a way to get the best of both of these approaches, or something else I've overlooked?

In general, I would probably favor the second method, since the first one becomes clumsy when generalized to more than two functions.

But I can't think of a concrete use case at the moment — letting the functions call f_foo and f_bar seems good enough to me. Can you show more context? Sometimes, specific contexts call for specific solutions.

1 Like

You could use an enum to represent the individual calls and an Iterator to represent a sequence of them: (Playground)

But there are more options, too, depending on what you are trying to accomplish. Two that come to mind are returning a closure or something that implements Drop.

use Thing::{*};

fn main() {
    for x in f_3() {
        match x {
            Foo(foo) => f_foo(foo),
            Bar(bar) => f_bar(bar),
        }
    }
}

fn f_3() -> impl IntoIterator<Item=Thing> {
    vec![Foo(5), Bar(false)]
}

enum Thing {
    Foo(u16),
    Bar(bool),
}

fn f_foo(foo: u16){
    println!("{}", foo);
}

fn f_bar(bar: bool){
    println!("{}", bar);
}

I like the enum method, but I tend towards having too many types if I'm honest :smiley:

I can see why you wouldn't want matches everywhere, but I'd avoid that by implementing that on the enum, something like:

impl Thing {
    fn run(self) {
        match self {
            Foo(foo) => f_foo(foo),
            Bar(bar) => f_bar(bar),
            FooBar(foo,bar) => {f_foo(foo);f_bar(bar);},
            Nothing => ()
        }
    }
}

Of course how this needs to be used might make that difficult.

fn main() {

//method one b
    let x = f_1();
    x.run();

//method three b
    f_3().into_iter().for_each(|x| x.run());
}

I haven't tested this and I'd be interested in what problems it has, if any :slight_smile:

EDIT: missing ()

You can also have something more hybrid:

pub struct MyThing(Option<u16>, Option<bool>);

with constructors

pub fn nothing() -> Self
pub fn foo(foo: u16) -> Self
pub fn bar(bar: bool) -> Self
pub fn foo_bar(foo: u16, bar: bool) -> Self

and a method

pub fn run(&self) {
    self.0.map(f_foo);
    self.1.map(f_bar);
}

Then your code in main becomes f_3().run(), where

fn f_3() -> MyThing {
    MyThing::foo_bar(5, false)
}

See it in action.

4 Likes

The hybrid approach is pretty much what I was looking for.

The functions f_foo and f_bar are the only ones that will ever be needed and they will always occur in the same order: foo then bar, so generalizing to an iterator seems like overkill.

To answer L_F: Currently I have private functions in a library that call f_foo and f_bar, which are also private functions. But there is a need for a user defined function outside the library to be able to mimic this behavior, but I don't want f_foo and f_bar public, nor do I want a user defined function to break the implicit contract by calling them out of order, or more than once, thus the change to a return type.

That sounds like you're providing some kind of cleanup routine that might need to run after user code. Does it make sense to return an RAII guard instead that will do the cleanup when the value is dropped?

You might also want to tag MyThing with the #[must_use] attribute. That'll prevent callers from accidentally ignoring one that gets returned from a function (though they can still explicitly ignore it).

1 Like

I have a very similar problem where I need to represent open/closed intervals ([a,b], [a,b), (a,b], (a,b) in mathematical notation) and I cannot easily decide between several possible implementations. They each have different pros & cons, forcing you to structure the rest of the code slightly differently, with different constructors and helper functions. In the end, I prefer the endpoint_kind, followed by wrapped, for ease of use, even if they take up more memory space, I think (because they need 2 tags instead of 1). I would like to know if some of you have opinions in this regard or suggestions.

You might want to take a look at RangeBounds from the standard library. Even if you don't end up using it directly, it may provide some inspiration.

1 Like

Yes, thank you; I was aware of it, but the main difference is that in that case RangeBounds is a trait and the different kind of intervals are independent structures. I believe in my situation I cannot afford the dynamic dispatch (I need to work with heterogeneous collections of intervals of different kinds), so I need a single interval type with tags indicating its topological properties.

Effectively, using (Bound<T>, Bound<T>) to represent an interval, as here, is more or less equivalent to my wrapped version.

Lacking knowledge of what you are attempting, and thus the distribution frequency of the various potential endpoint checks and computations, I would start coding with your enumeration::Interval and plan to refactor once the pros and cons of each alternative became more obvious from your coding experience.

1 Like

I'm implementing a type representing a finite union of intervals, with union, intersection, complement operations. The biggest complication is to deal with generic types that can be bounded (u8, characters, an enum...), unbounded (f64, with ±inf representing the endpoint of unbounded intervals), and mathematically unbounded but with bounded computer representatives (i32). I still have to figure out how to capture nicely all this possible variations with a single elegant type.

I also started with enumeration::Interval, but pattern matching on two intervals can become unwieldy. Imagine a function

fn merge(int1: Interval, int2: Interval) -> Option<Interval>

that checks if the union of int1 and int2 is an interval and if so returns it. I know that you can work around it by having some fn start(&self) -> &T and fn end(&self) -> &T helper methods, but I hope you see that it was not very easy for me to know a priory which approach would be better. I guess I have wasted more time evaluating every possibility and actually finished coding neither :laughing:

If you're also dealing in unbounded intervals then RangeBounds with its start_bound(), end_bound(), and contains() methods is the way to go.

It's not cleanup, just a user supplied custom function that can optionally trigger one of those two private functions.

Edit: Just want to say thanks to everybody for the ideas. I am usually amazed by the level of good advice posted here.

I actually have a pair of crates doing exactly that, which might be worth reviewing.

https://crates.io/crates/few
https://crates.io/crates/normalize_interval

pattern matching on two intervals can become unwieldy.

yeah...

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.