Interpreting trait bound advice from compiler

I was trying to write a generic function that can join a slice of either owned strings or string slices. I didn't know what the appropriate trait bound should be, so I started with AsRef<str>:

fn join<S>(slice: &[S]) -> String
where
    S: AsRef<str>,
{
    slice.join(" ")
}

But that yielded this compile error:

error[E0599]: no method named `join` found for reference `&[S]` in the current scope
 --> src/main.rs:5:11
  |
5 |     slice.join(" ")
  |           ^^^^ method not found in `&[S]`
  |
  = note: the method `join` exists but the following trait bounds were not satisfied:
          `<[S] as std::slice::Join<_>>::Output = _`

I'm not sure how to interpet the hint on which trait bounds were not satisfied, i.e. <[S] as std::slice::Join<_>>::Output = _. Eventually, I decided to look up impls of std::slice::Join in the docs, and there I noticed that the one I need has a trait bound on S: Borrow<str>, so I changed my own trait bound to that and that did the trick.

However, I still don't understand how I should have inferred that from the <[S] as std::slice::Join<_>>::Output = _ hint. Any advice on how I should read this type of hint in the future would be greatly appreciated :slight_smile: What is the compiler trying to tell me by including this information?

You have two other ways of making this work:

fn join_without_allocation<S>(slice: &[S], sep: S) -> String
where
    S: AsRef<str>,
    [S]: std::slice::Join<S, Output = String>,
{
    slice.join(sep)
}

fn join_with_allocation<S>(slice: &[S]) -> String
where
    S: AsRef<str>,
{
    slice.iter().map(|s| s.as_ref()).collect::<Vec<_>>().join(" ")

where the former needs nightly for now to enable #![feature(slice_concat_trait)].

Note in the first case that the separator is coming as an argument because you can't turn a &str into S in the fn body, you can only ask the type S to give you something by calling an associated function or referencing an associated const (or passing it in like here).

1 Like

Thank you for the reply! Seeing these other options is helping me wrap my head around trait bounds a little better, so your time is much appreciated :slight_smile: Bounds always seem relatively straightforward when reading about them, but when writing my own generic code, I quickly get out of my depth.

So is #1 what the compiler was trying to nudge me towards with that hint? I'm still not entirely sure how I should have figured out I need a [S]: std::slice::Join<S, Output = String> trait bound from <[S] as std::slice::Join<_>>::Output = _. I was sort of groping in that direction (though I didn't figure out that the bound should be on [S]), but when I realized that I can't refer to std::slice::Join without using nightly, I backed out.

This point was especially instructive, thank you for that! It clears up what you can/can't do with type parameters.

I tried restoring the hardcoded separator to see the error I'd get -- expected type parameter S, found &str, which makes a lot more sense now -- and realized it can be made to work like so (for completeness' sake):

fn join_without_allocation2<S>(slice: &[S]) -> String
where
    S: AsRef<str>,
    [S]: std::slice::Join<&'static str, Output = String>,
{
    slice.join(" ")
}

(For anyone coming here looking just for practical advice: the solution in OP with fn join<S: Borrow<str>>(slice: &[S]) -> String still is simpler and doesn't require nightly.)

As for your method #2, I think I vaguely realized something like this ought to be possible but didn't really explore the option, since as long as I'm jumping through hoops to make generic code work, I'd rather avoid an unnecessary allocation :slight_smile:

Glad to hear!

You might want to read about the fully qualified syntax, which is the unambiguous way to refer to all items.

For example, let's say we have

struct S;
trait T {
    type AssocTy;
}

impl T for S {
    type AssocTy = usize;
}

you can refer to AssocTy in multiple ways: S::AssocTy works because there's only one trait with an associated item with that name. This is internally equivalent to <S as _>::AssocTy. If there were more than one trait impled for this struct with an associated item named AssocTy, then you would have to write <S as T>::AssocTy. That is the fully qualified syntax, and what rustc is showing. But when using S or T you normally will have to specify the associated type on its use. In this case, if you were to take this trait as a function argument, you would write something like fn foo(_: &T<AssocTy = usize>), which would accept S. But if you want the assoc type to be generic, you would have to write a bound: fn foo<X>(_: &T<AssocTy = X>) or fn foo<X, Y>(_: X) where X: T<AssocTy = Y> if you need static dispatch. This last one is equivalent to the currently unsupported fn foo<X, Y>(_: X) where X: T, <X as T>::AssocTy = Y (using =/equality in where bounds isn't supported, only :/restriction).

Also, when it comes to where trait bounds, they are more powerful than they initially seem, because you first encounter them only restricting type parameters, but as shown here the left-hand side of the : can introduce obligations for rustc to resolve that are more involved.

I do recommend people that are starting up to not try to learn the whole language at once. If you delay learning how to write, lets say, zero-allocation code, you can get proficient with other parts of the language and when you try to do this you'll have better understanding of the error messages that are general, the ones that are specific to the problem at hand, and how the different features fit together :slight_smile:

2 Likes

I've read about FQS, I just found it confusing in the context of a trait bound hint :slight_smile: I suddenly wasn't sure whether I might be unaware of some syntax involving = in where clauses, which, as you helpfully point out, doesn't actually exist but would be a reasonable alternative for expressing the same constraint ( cf. your example fn foo<X, Y>(_: X) where X: T, <X as T>::AssocTy = Y).

I guess what I'm trying to say is that in retrospect, I would've expected the trait bound hint to look something like [S]: std::slice::Join<_, Output = _> instead of <[S] as std::slice::Join<_>>::Output = _, because that's closer to what you have to type. But there's probably a reason why it's the latter.

(Actually, the hint should ideally be S: Borrow<_> (I'm not quite sure what those _'s mean -- are they places where the compiler doesn't have enough information and it's up to the programmer to decide?). That's the simplest solution, but I understand this might be hard/impossible for the compiler to reason through.)

You're absolutely right of course :slight_smile: The trouble is, I enjoy reading about Rust, so I know in theory about many things that should be possible, which solutions are elegant etc. However, I have comparatively little practical experience. So whenever I write something, I'm either unhappy because I'm painfully aware I'm doing it the dumb way, or frustrated because I constantly trip myself up trying to do the fancy thing. But it's slowly getting better.

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.