Mut String += format_args!(…);

We have Add<&str> for String already along with the AddAssign<&str> for String, I would expect the same pair here.

2 Likes

Huh. TIL

Just my 2c, but I'm not fond of s += format_args!(...) (or s.bikeshed(format_args!(...)) for that matter). It feels a bit too magical, and forces the user to learn about format_args which in all other cases is merely an implementation detail. Also, it's a bit too easy to mix up with s += format!("...") which has exactly the same sematics, is just less efficient (although I guess a lint would help here).

On the other hand, trying to write! to a String may not be immediately obvious, but it should be pretty obvious in retrospect and pleasantly orthogonal. It should just be documented better – currently there's nothing in the String docs that point to that direction besides the very undiscoverable Write impl at the very bottom of the page. At least the docs for add_assign and push_str could be updated to mention the write! idiom. There's also the very recently added Clippy lint format_push_string although it's allow by default.

2 Likes

But using the write!-trait comes at the inconvenience of an unecessary and unreachable error path. Two potential alternatives:

  • It could live as a method on fmt::Arguments.

  • The ToString trait could have a method accepting an existing buffer for append. It's implemented with a specialization for fmt::Arguments as-is already, this could then also be sutiable specialized for performance.

  • On that note, an appending method on ToString would make the usage Display potentially more efficient. Since, as impl<T: Display> ToString for T puts it:

        let mut buf = String::new();
        let mut formatter = core::fmt::Formatter::new(&mut buf);
        // Bypass format_args!() to avoid write_str with zero-length strs
        fmt::Display::fmt(self, &mut formatter)
    

    Shouldn't it be somewhat certain that the same applies to the write!? Since this is, immediately, constructing a format_args! internally that would run into w/e issue in codegen the above faces.

    For argument's sake let's call such a speculative new method

    trait ToString {
        default fn write_string(&self, buffer: &mut String) { //…
        }
    }
    
1 Like

Yeah, I meant (but failed to explicitly state) that some way of resolving the write! error situation (even if hackish) would be preferable to the format_args! alternative. What about simply an annotation like allow(unused_result)?

Previous discussion here:

Since then, I became even more convinced that this is a trivial papercut which is important to fix in stdlib.

I like += format_args!() solution because its a clear improvement with basically no downsides. We should go and do it.

I wouldn't want for us to stick with buf += format_args!() as a long-term (10 years from now) solution --- its a clever use of plumbing and a happy coincidence in library design, but it doesn't seem like a sufficiently good first class solution.

Potential long-term alternative is for rust to gain first-class string interpolation syntax, such that we could write

buf += f"Hello, {name}!"

where f"" desugars into something like format_args!.

6 Likes

Another thread I created a while ago: Method to append any Display type to a String

I would really prefer an inherent method over a += impl, because

  • it is less magical
  • it is more general (it can be generic over Display, whereas a blanket impl<T> AddAssign<T> for String where T: Display is not possible)

In the previous discussion, there were only two open questions: The name, and whether the method should accept T or &T (though most people argued for &T).

1 Like

I don't see any reason why we shouldn't just do both.

As for the name question, I think push_display is the obvious choice. While it's a little clunky, it's also very clear from the name what it happening.

4 Likes

write! is quite magical for Rust. It is completely duck-typed — it doesn't use Write traits, it takes anything that compiles with .write_fmt(args) appended to it. It doesn't even care about the function type or return type. It takes even bogus signatures like fn write_fmt(&self, f: impl Sized) -> ().

Compared to that format_args! is pretty tame and well-behaved.

If format_args! could be made to borrow its arguments for a longer lifetime… (eh, can't).

8 Likes

If a new operator ++ for concatenation strings would be added, then ++= also could be added

Strings can already be concatenated with + and += (with other strings). What would the operator ++ do differently?

The question here is if it a good idea to allow other right-hand-sides (impl Display or fmt::Arguments) for + and +=.

3 Likes

As a humble bikeshedding enthusiasist I'm respectfully reminding that String::write() is not occupied in any popular way I'm aware of.

3 Likes

Are there other parts of the stdlib that are duck typed like write!?

It's very frustrating to work with and doesn't feel likes regular rust code at all.

It would be much better to have a trait to abstract away the two Writes (std::io::Write and std::fmt::Write)

There is also an RFC for postfix macros which would be another way to tackle this possibly, resulting in something like buffer.write_to!("{x}"). Not sure if I personally like this though, mainly raising it as another possible option.

4 Likes

I just had a look at that, the problem is that using a qualified path we lose out on the autoref behavior of $buf.write_fmt

It's certainly not great, but format_args! is definitely already more than an implementation detail, and is already worth learning about for some use-cases. I've written code that calls it directly on multiple occasions when I wanted to avoid the intermediate String allocations caused by format!. The name is a bit suboptimal (format_lazy! or so would better reflect what happens), but oh well.

3 Likes

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.