Extracting a value from an enum variant

So I've been experimenting with extracting values from enum variants. I have a workable solution, but it seems clumsy and misses what I really want to do. Here's my code and then I'll explain myself:

#[derive(Debug)]
pub enum TypeWrapper {
    Alphanum(String),
    Letter(char),
    Integer(i64),
    Floating(f64),
}

impl TypeWrapper {
    pub fn util_extract_typewrapper(value: TypeWrapper) -> String {
        match value {
            TypeWrapper::Alphanum(i) => i,
            TypeWrapper::Letter(i) => i.to_string(),
            TypeWrapper::Integer(i) => i.to_string(),
            TypeWrapper::Floating(i) => i.to_string(),
        }
    }
}

fn main() {
    let something = TypeWrapper::Letter('m');
    println!("\n The wrapped up value is:   {:?} \n", something);

    let value = TypeWrapper::util_extract_typewrapper(something);
    println!("\n The value as a String is :   {} \n", value);
    
    value.chars().next().unwrap();
    println!("\n The value as a char is :   {} \n", value);
}

This works, but forces me, in the calling function, to deal with converting the extracted value from String into the actual type wrapped up in the enum variant. What I really want to do is pass a TypeWrapper variable to an extraction function that will then, instead of returning only Strings, return to me the value in the actual type the enum variant is wrapping. So, I'm actually asking two questions here this morning:

  1. Disregarding my angst concerning where and when to convert each variant into the given type, is this code okay, or is there a better way to do this?

  2. Is there a way for an extraction function to return the value in the actual type the enum variant is wrapping? (I tried using generics and couldn't get past the compiler complaining that it must know the type being returned at compile time.)

Thanks!

Rust is statically typed, and so it forces you to specify a unique type as the return type for functions. You can either use another enum to represent the union of the possible types to be returned (String or usize), or a trait object.

4 Likes

If I understand you correctly, then the thing of it is that if there is at least 1 variant that wraps a different type from the others, the there is no function can do this. The reason is that functions need to return a value of the same type¹ for each returning branch.

What you could do however, is define a trait with the functionality you need, and then have the converting function return Box<dyn MyTrait>², if you can live with the additional overhead of the allocation.

¹ I'm purposely not counting diverging types like ! for the purposes of this conversation
² impl MyTrait won't work because that requires all of the values to be of the same type. It's just that that type is unnamed.

2 Likes

A single expression must have a single type, and the singular inner fields of each of your enum variants do not have a type in common.

You can have multiple expressions:

match tw {
  Alphanum(i) => todo!(), // i has type String or &String
  Letter(i) => todo!(),   // i has type char
  Integer(i) => todo!(),  // i has type i64
  Floating(i) => todo!(), // i has type f64
}

Or, as you have done, you can define conversion operations that convert from the disparate types of the different enum variants to some common type (in your util_extract_typewrapper example, String). Or, if neither is appropriate, you can reconsider the definition of TypeWrapper in some way, though I couldn't speculate as to how without a better picture of how it's used in your program.

2 Likes

Since a value of only one type can be returned, it would need to return an enum with one variant for each type, and then you're back where you started. :slight_smile: So instead of extracting the value, you can do a match and then do whatever work you need to do for each type, inside that variant of the match.

Also (edited in), if you don't know what work you'll do until later, you can pass the enum itself around to the parts of your program that need it, and then do the match at the time you need to do the work.

2 Likes

Well, to provide a better understanding of what I am doing with this, consider that, in actual application, this line in main()

let something = TypeWrapper::Letter('m');

would actually be more like this:

let something = choose_item("chars");

where the function choose_item() randomly picks an element from a vector that is filled with either Strings, chars, i64, or f64 values. The function returns that randomly chosen element wrapped up in a TypeWrapper enum variant. (Having it return the TypeWrapper type satisfys the compiler's "only one type returned" rule.) Once I have that value returned from choose_item() I then have to extract the value so that I can use it elsewhere in my program. And, yes, I guess what I'm asking for is a way around the compiler's "only one type returned" rule.

That use is what @derspiny was asking about.

Right now this is an XY problem, because you're asking for a near-impossible thing (return different types from a function), without telling about your actual use case that probably has a different solution.

2 Likes

Once I have that value returned from choose_item() I then have to extract the value so that I can use it elsewhere in my program. And, yes, I guess what I'm asking for is a way around the compiler's "only one type returned" rule.

It's more a "statically typed" rule.

// `value` must have a single, statically known type
let value = extract(the_enum);
use_it_elsewhere(value);

The rule always applies; Box<dyn Trait> and enums, etc, are still statically known types. They just offer a way to work with multiple base types behind their own single type.


I don't know if it will work for you, but perhaps you can define a trait that does useful things...

trait DoSomethingUsefulWith<T> {
    fn use_it_elsewhere(self, value: T);
}

...have some type that implements it for all your value types...

struct Example;
impl<D: std::fmt::Display> DoSomethingUsefulWith<D> for Example {
    fn use_it_elsewhere(self, value: D) {
        println!("The value is: {value}");
    }
}

...and then dispatch from each variant to the appropriate implementation:

impl<F> DoSomethingUsefulWith<TypeWrapper> for F
where
    F: DoSomethingUsefulWith<String>,
    F: DoSomethingUsefulWith<char>,
    F: DoSomethingUsefulWith<i64>,
    F: DoSomethingUsefulWith<f64>,
{
    fn use_it_elsewhere(self, value: TypeWrapper) {
        match value {
            TypeWrapper::Alphanum(v) => self.use_it_elsewhere(v),
            TypeWrapper::Letter(v) => self.use_it_elsewhere(v),
            TypeWrapper::Integer(v) => self.use_it_elsewhere(v),
            TypeWrapper::Floating(v) => self.use_it_elsewhere(v),
        }
    }
}
1 Like

Don't use TypeWrapper for this.

Instead the function choose_item should be parametrized by a generic type, for example:

fn choose_item<T>(values: &[T]) -> &T

playground

9 Likes

All else being equal, I would expect choose_item to be generic, taking a type parameter that relates the type of its arguments to the type of its return values. This kind of thing is pervasive in Rust; one possible implementation of choose_item might be

use rand::seq::IteratorRandom;

fn choose_item<T>(items: impl IntoIterator<Item=T>) -> Option<T> {
    let mut rng = rand::thread_rng();
    items.into_iter()
        .choose(&mut rng)
}

which, when given any collection of Strings, returns an Option<String>, but when given a collection of i64s, returns an Option<i64>, instead. The body still works with a single type, T, but there is effectively an implementation of the function for each T you actually use in your program, without requiring any other relationship between those types. (This is also generic on the type of items, which allows it to accept both slices, vectors, and most other sequence types, but you could simplify that to Vec<T> if you find it clearer.)

If the vector is itself heterogeneous and its values don't all share a type, then that heterogeneity needs to be handled in some other way. @quinedot pointed out a couple of ways that Rust can put a single type in front of heterogeneous values, subject to a few constraints, and an enum is also a valid way to do that - but in any case the collection you're actually working with is a collection of that outer type, and not a mixed collection of the values behind that type.

1 Like

What are the specific benefits of using traits? From my perspective, it seems that dispatching to the concrete type contained within TypeWrapper must be done regardless. Once the concrete type is known, it can be used, even for generic functions:

pub enum TypeWrapper {
    Alphanum(String),
    Letter(char),
    Integer(i64),
    Floating(f64),
}

fn use_it_elsewhere(value: TypeWrapper) {
    match value {
        TypeWrapper::Alphanum(v) => inner(v),
        TypeWrapper::Letter(v) => inner(v),
        TypeWrapper::Integer(v) => inner(v),
        TypeWrapper::Floating(v) => inner(v),
    }
    
    fn inner(value: impl std::fmt::Display) {
         println!("The value is: {value}");
    }
}

fn main() {
    let something = TypeWrapper::Letter('m');
    use_it_elsewhere(something)
}

Personally, I find this approach simpler to read and understand, and it also seems more concise.

1 Like

If you want to do the useful thing (fn inner) more than once, you'll separate it out anyway; if you have a repeating pattern of dispatching to the inner types, the match-and-dispatch matches you'll have throughout your code become repetitive noise. Having a trait and a dispatching implementation encapsulates the boilerplate mechanics of getting the logic you actually care about done. And concentrates it as you add methods or variants.

1 Like

That's all some really good stuff, everyone. Thanks for your input. :grinning:

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.