9

Should I be writing:

impl<'a> From<&'a Type> for OtherType

Or should it be

impl From<Type> for OtherType

I'm having a difficult time finding the answer, perhaps due to a vocabulary failure on my part. I really don't particularly care about the reference-ness/value-ness of the argument.

In C++, I would define the function over/method on values and calling it on const references.

Is there an automatic derivation from impl Trait<Type> to impl<'a> Trait<&'a Type>?

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
jcc333
  • 759
  • 5
  • 15
  • 1
    This simply isn't answerable in its current form; it depends on why the trait needs a type parameter in the first place. Do you mean specifically the [operator traits](https://doc.rust-lang.org/std/ops/) like `Add`, `Sub`, `BitAnd`, etc.? – trent Jan 13 '18 at 23:39
  • Edited to *try* to clarify, but I'll be honest, coming from a functional programming / c++ background some of this stuff is still fairly inscrutable so I'm kind of shooting in the dark – jcc333 Jan 13 '18 at 23:59

3 Answers3

5

Should Rust implementations of From/TryFrom target references or values?

Yes, they should.

There's no trickery here: implement the traits to convert whatever types you have. If you have a String, implement it to convert from Strings. If you have a &str, implement it to convert from &strs. If you have both, implement it for both.

Is there an automatic derivation from impl Trait<Type> to impl<'a> Trait<&'a Type>?

No, and for good reason. For example, consider this conversion:

struct Filename(String);

impl From<String> for Filename {
    fn from(value: String) -> Filename {
        Filename(value)
    }
}

There's no obviously correct way for the compiler to implement that for a reference to a String. However, you can implement it yourself:

impl<'a> From<&'a str> for Filename {
    fn from(value: &'a str) -> Filename {
        String::into(value.to_owned())
    }
}

If you can't make use of the incoming allocation, then there's not much reason to accept the argument by value, so you might as well accept a reference. However, I'd say it's less common to use From for such conversions — not unheard of, though.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • This answer seems like it dodges the question by focusing on `&str` and `String`. When I look at existing code it seems very common for `From` to be implemented on the value, which means it consumes the input, even though virtually no `from()` functions actually need to do that. But if you implement it on the reference then users who have the value have to constantly do `(&val).from()`, which is clunky – Michael Mrozek Mar 08 '23 at 22:52
  • @MichaelMrozek I don't see anything in the answer that's specific to `String`, I only chose it because it's an existing type from the standard library. The main thing is to look at what types you have, what types you want, and how best to reduce needless resource creation. In many times, you can take advantage of existing ownership (e.g. `String` -> `Vec`) or want to make it obvious that allocation is occurring (e.g. `my_type.clone().into()`). – Shepmaster Apr 14 '23 at 21:00
  • @MichaelMrozek There are situations where this clunkiness is unavoidable, and generally it would be `(&val).into()` in practice; for the `from` counterpart it might be called as `DstType::from(&src)` which may make it more clear but wordier. In any case I've written an answer (my first direct Rust related one, at that) that goes into why one might `impl From<&SrcType>` that might have practical usage (with a small step outside `str` and `String` despite still using those at heart). – metatoaster May 27 '23 at 00:10
1

In the following case, I would say "only implement From<Src>, not From<&Src>":

// These types are NOT Copy! (Nor should they be.)

#[derive(Clone, ...)]
struct Src {
    v: Vec<u8>,
}

#[derive(Clone, ...)]
struct Dst {
    v: Vec<u8>,
}

My rationale: Suppose someone has a &Src, and they want a Dst. That can be done very easily without From<&Src>:

Dst::from(src_ref.clone())

You might say, "But you are forcing the user to clone!". True, but you would need to clone at some point anyway:

impl From<&Src> for Dst {
    fn from(src: &Src) -> Dst {
        Dst {
            v: src.v.clone(),
        }
    }
}

Alternatively,

impl From<&Src> for Dst {
    fn from(src: &Src) -> Dst {
        Dst::from(src.clone())
    }
}

Either way, you haven't avoided cloning.

Thm 1: if you have a &Src and you want to make a Dst, you will ALWAYS clone at some point. Implementing From<&Src> does not save you from that inevitability.

At the same time, you have nearly duplicated code. Look back at your From<Src>:

impl From<Src> for Dst {
    fn from(src: Src) -> Dst {
        Dst {
            v: src.v,
        }
    }
}

Looks really similar to the first impl of From<&Src>, right?

If we did the other way around (i.e. implement From<Src> in terms of From<&Src>), then we would always clone (this is just a corollary of thm 1).

Whereas, if we just implement From<Src>, the caller MIGHT be required to clone, but they might also have the opportunity to hand over ownership of Src, which would result in just a move instead of a clone.

Ok, but what if the caller has a Src, not a &Src. There are two subcases here:

  1. The caller wants to hang onto their copy of Src.
  2. The caller is ok with handing over ownership to Dst::from.

Of course, in the latter case, they should use our From<Src>.

But what about case 1? If we stick with my advice, it seems we again force the caller to clone:

Dst::from(src.clone())

If only we could do

Dst::from(&src)

! But look back to thm 1: we haven't actually avoided cloning. We just let From<&Src> do the dirty work of cloning for us. All we have done is swepted the clone under the rug, so to speak.

I think that covers all the (interesting) cases. I didn't really cover the case where Src and Dst are Copy, because that's not really "interesting". That's "not interesting" because there are no expensive clones to get rid of. The only thing you might have to do is use the dereference operator:

Dst::from(*src_ref);

But that's not a performance issue. The only "expense" of doing that is typing one more * character, which really won't kill you.

The main "problem" with my advice is that people will look at your clones and think, "I know how to get rid of this! I'll just add impl From<&Src>!". But it turns out, this is a fools errand.

allyourcode
  • 21,871
  • 18
  • 78
  • 106
0

To extend upon the very excellent answer that @allyourcode wrote, specifically to cover the situation where From<&SrcType> will not make an implicit copy or clone: if the goal is to build a DstType with references to fields that live within SrcType (i.e. DstType to provide a view into SrcType), for example, using the following newtypes:

struct EmailAddr(String);    // e.g. user@example.com
struct Domain<'a>(&'a str);  // a view onwards from the @ symbol of the above

To build an impl that go from EmailAddr into the view Domain<'a>, something like this may be done (admittedly this is a rather contrived example that made wrong assumptions about the structure of an email address for simplicity sake):

impl<'a> From<&'a EmailAddr> for Domain<'a> {
    fn from(value: &'a EmailAddr) -> Self {
        Self(&value.0[value.0.find('@').unwrap_or(0)..])
    }
}

Then something like the following may be done to demonstrate how the above might work (playground link):

let email = EmailAddr("user@example.com".to_string());
let domain = Domain::from(&email);

To extend the above example further, if there is a function that would consume a Domain like so:

fn consume_domain<'a>(domain: Domain<'a>) {
    println!("domain {:?} consumed", domain);
}

The above function may be called by producing a Domain using .into() with an EmailAddr reference like so:

consume_domain((&email).into());

Note the parentheses around the value (&email) - this is to ensure this is the reference of the value calls .into(), otherwise it is interpreted as turning the whole EmailAddr into the target type and then taking the reference of that. Yes, this may feel clunky, but is necessary to disambiguate these two different interpretation.

For a more practical example, there may have a newtype that encapsulates a vector of 2-tuples of name and email address for a simple address book. To facilitate doing a large number of lookups, a good way is to create a HashMap out of that data by referencing (pointing to) the data that lives inside the Vec<(String, String)>. Cloning all the Strings out of the vector into the map is always possible, but doing so will more than duplicate memory usage (or worse in the case of also providing a reverse lookup map; really it's better not to clone potentially long strings at all). For example:

struct ListOfUsers(Vec<(String, String)>);  // (User, Email)
struct UserToEmail<'a>(HashMap<&'a str, &'a str>);
struct EmailToUser<'a>(HashMap<&'a str, &'a str>);

impl<'a> From<&'a ListOfUsers> for UserToEmail<'a> {
    fn from(value: &'a ListOfUsers) -> Self {
        Self(value.0.iter()
            .map(|(u, e)| (u.as_ref(), e.as_ref()))
            .collect::<HashMap<&'_ str, &'_ str>>()
        )
    }
}

impl<'a> From<&'a ListOfUsers> for EmailToUser<'a> {
    // same fn from as above but with user/email swapped
}

Conceptually, the HashMap<&'a str, &'a str> functions as a view into the Vec<(String, String)>. Creating a view using a From<&...> just solidifies the fact that making a view does involve some form of non-view related conversion (simply by the virtue of needing an allocation of something concrete that holds onto the view, e.g. HashMap).

Example usage may look something like this (completed example at playground link):

let users = ListOfUsers(vec![
    ("John".into(), "john@example.com".into()),
    ("Mary".into(), "mary@example.com".into()),
    ("Takao".into(), "takao@example.com".into()),
    ("Darcy".into(), "darcy@example.com".into()),
]);
let user2email = UserToEmail::from(&users);
println!("{}", *user2email.0.get("Darcy").unwrap());  // outputs darcy@example.com
let email2user = EmailToUser::from(&users);
println!("{}", *email2user.0.get("mary@example.com").unwrap());  // outputs Mary
metatoaster
  • 17,419
  • 5
  • 55
  • 66