Coercion of &str to enum

I have a:

enum Identifier {
  Name(String),
  Uuid(uuid::Uuid)
}

If I have a function:

fn operate_on(id: Identifier) {
  match id {
    Identifier::Name(n) => println!("Name: {}", n),
    Identifier::Uuid(i) => println!("Uuid: {:?}", i)
  }
}

Is there some trait magic one can use to allow this:

fn foo() {
   operate_on("hello"); // becomes an Identifier::Name()
}

I have a feeling that this can't work .. but how about:

fn foo() {
   operate_on("hello".to_string()); // becomes an Identifier::Name()
}

?

There's no trait you can implement to get coercions. Rust is highly coercion-averse.

That said, you can use the "Into trick". Generally I would say you shouldn't -- just have the caller pass the enum or call .into() themselves -- but something like this works:

enum Identifier {
  Name(String),
  Uuid(i32)
}

impl From<&str> for Identifier {
    fn from(x: &str) -> Identifier { 
        Identifier::Name(x.to_owned())
    }
}
impl From<i32> for Identifier {
    fn from(x: i32) -> Identifier {
        Identifier::Uuid(x)
    }
}

fn operate_on(id: impl Into<Identifier>) {
  match id.into() {
    Identifier::Name(n) => println!("Name: {}", n),
    Identifier::Uuid(i) => println!("Uuid: {:?}", i)
  }
}

fn main() {
    operate_on("hello");
    operate_on(4);
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=e48ca845faeaf4f811c570899b8f45d6

2 Likes

Thank you! But before I decide whether to use this I'd like to understand why it is discouraged.

I never asked or tried to figure out how to do it previously because I've never felt the need. However, while working with an API I developed lately I notice that virtually every call in the application is made using the Name format. The Uuid representation exists because the underlying interface supports it, but I never use it, and I am guessing that it would very rarely be used by anyone else. While I want the Uuid feature for completeness, I feel that Identifier::Name() is way too much clutter due to the regularity if its use.

With that in mind; I'd like to know when it is and when it isn't appropriate to use that Into trick. What are some types of cases when people feel it is misused? And what are perfectly legitimate uses of it? And perhaps most importantly -- why? It because an extra conversion is done for each call?

That's a good question. A couple of things:

  • Making every function generic that way is bad for compile times. That's because instead of compiling them once in the library, they have to be monomorphized on use in the application. You can refactor everything to be outlined to limit that, but it's questionable whether that's worth the effort.
  • Making everything generic means that you can't use anything dependent on inference context when calling it. For example, foo(Default::default()) or foo(s.parse()) won't work with the impl trick (those two specific examples might not make sense for your case, but they're the easiest ones to explain).
  • Making the str->String allocation invisible tends to lead to "pervasive pessimization": something that's not really bad, but you lose the opportunity for the weak push to "did you maybe want a variable to not do that repeatedly?"

I agree. So what I would say is that the From impls are still good (as well another for From<String> probably), it's just the invisible calling of them that I suggest avoiding.

That means that you can always just do operate_on("hello".into()), which is fairly minor overhead. And if you're going to call multiple things with the same id, it encourages let id = "hello".into(); foo(&id); bar(&id);.

Oh, that's another good point: most of these should probably be taking &Identifier, since you don't need to consume the String every time. And operate_on(&"hello".into()); will work, but impl Into<&Identifier> isn't going to because there's nowhere to borrow it from.

TL/DR: Keep the Froms, just have the caller do the .into() instead of impl Into<_>ing everything.

2 Likes