Feature request: std::fmt::Write::write_line

hexchat-plugin has a print(impl ToString) which is a line-writer. It doesn't support the equivalent of write_fmt. It'd be nice if write_fmt and write_line were separate so that our write_fmt could be unimplemented!() and write_line didn't push a newline at the end (altho hexchat doesn't care about a terminating newline, and in fact slightly prefers it, but w/e).

This would be used by writeln!.

You can use something like a LineBuffer that flushes on newlines to support such a writer.

std::fmt::Write is supposed to have a write_str but it'd be nice to have that be unimplemented!() and also have write_fmt be unimplemented!() and also have write_char be unimplemented!() and instead have write_line implemented.

this is the source code for the crate, consider taking a look at it: lib.rs - source we had an argument about this very issue on the community discord, also note that the default write_fmt calls write_str multiple times which is extremely less-than-ideal for this use-case and we're also not sure if there's any guarantee that write_fmt doesn't get called multiple times.

Any opposition to making writeln! use a new write_line method?

That would break anyone expecting writeln!() to call .write_fmt().

write_line would call write_fmt.

How would that work? write_fmt takes std::fmt::Arguments. How would writeln! pass them to write_fmt if it is calling write_line?

Why not just implement write_fmt and ignore the args yourself? (Or use another trait that doesn't have unimplemented methods that will panic at the drop of a hat?)

write_line takes fmt::Arguments, and calls write_fmt with another fmt::Arguments.

trait Write {
  ...
  fn write_line(&mut self, f: std::fmt::Arguments<'_>) -> Result<...> {
    self.write_fmt(format_args!("{}\n", f)) // or something
  }
}

The whole point is to be able to do

impl Write for Foo {
  fn write_fmt(...) -> ... { unimplemented!() }
  fn write_line(...) -> ... { actually does stuff }
}

How does that help you?

Doesn't that make write_line just as useless to you?

It sounds like you want writeln! to not write its newline, but for write! to just be broken? What happens if someone puts in a newline manually?

What if you use writeln! on the following type:

struct Foo:

impl Foo {
    fn write_fmt(&mut self, fmt: Arguments) {}
}

This type doesn't have a write_line method, so if writeln! were to be changed to use write_line, it would break on this type.

writeln! is for std::fmt::Write and std::io::Write not for your Foo.

The point is that it's a line writer. It doesn't support non-line writes. And indeed, it'll accept embedded newlines but they'll behave slightly different from "lines".

But the main issue is that write_fmt doesn't work for this.

This has the exact same function signature of write_fmt. This is why I don't understand why write_fmt "doesn't work". You can make it do whatever you need it to do. You can defer to other traits. You can have other traits defer to it. You can wrap writers in constructs that ensure specific behaviors. Changing the function's name doesn't do anything.

Moreover, you're suggesting that you want every existing method on Write to be unimplemented!. This is a huge flag indicating that this is not the trait you want to use.

These discussions are meant for us to find the best solutions to problems, not to ram through half-baked designs just because you insist your problem space is so inflexible that only what you propose will work. There are many ways to solve your problem. I'm trying to figure out what those are, and it's not helpful when you keep insisting this is 'against the whole point' of what you're trying to do. That makes it sound like your whole point is to win an argument.

2 Likes

write_fmt doesn't write lines.

writeln writes lines.

writeln currently uses write_fmt to write lines, but that's not what we want. our concept of line isn't based on newline characters in the content. writeln is what everyone is used to for writing lines tho, so it makes sense to use it.

writeln!:

"Write formatted data into a buffer, with a newline appended. On all platforms, the newline is the LINE FEED character ( \n / U+000A ) alone (no additional CARRIAGE RETURN ( \r / U+000D )."

It doesn't make sense for writeln! to do something other than what it is documented to do. If you have some other concept of a line, that should be implemented at a lower level, like std::io::write.

Nope, it is actively being depended on. Eg nushell, RustPython and materialize. Changing this is a breaking change and as such not allowed without a very good reason. A very good reason would pretty much only be an important bug fix. A new feature like this is not a good reason. You can write your own writeln!() if you want this feature for yourself, but the libstd writeln!() is unlikely to change.

5 Likes

That is somewhat concerning, but honestly, just...

write! and writeln! have to be macros because of formatting, and they're also the idiomatic way to format stuff into an object. unfortunately not all objects can work with the semantics demanded by write! and writeln!, and then ergonomics kiiinda go out the window because you have to use thing.print(format_args!(...)) which is so much more verbose and has more nested parentheses than writeln!(thing, ...). also there's currently no guarantee that writeln! calls write! (and thus write_fmt) only once, so you can't rely on that either.

for the hexchat-plugin crate, you already have multiple levels of indentation due to all the stuff about relying on the stack/lifetimes to provide memory safety, so lines like ph.print(format_args!(...)) really eat your horizontal space. being able to writeln!(ph, ...) would solve that, and the only way we can think of to make that acceptable to ppl is if there was such a fmt::Writer::write_line or something. (nobody has addressed this yet...)

write_line just makes more sense, from a more conceptual perspective, than what we have today. it has all sorts of advantages over what we have today.

(also macros are scoped/namespaced now. we could have a v2 write/writeln that isn't part of the prelude.)

So why not ask for that? It makes total sense to specifically promise that (especially since it falls under observable behavior and the "it just does w.write_fmt" that is relied upon and (mostly) guaranteed. This would be a simple docs PR with an FCP, then merged, probably resolved faster than this thread, and definitely faster than any extensions to the Write interface.

If this is guaranteed (which, depending on consensus, it might already effectively be!), then you have two clean options to interface with your sink:

  • Have users use write! for each unit chunk pushed to your sink. I think this is fair enough, especially if you want to preserve internal newlines.
  • Have users use writeln!, and buffer writes, which you then pass along to your log backend after splitting on newlines.

It's not like fmt::Arguments gives you any introspection power anyway, so you eventually have to dump it into a string (or another write sink, I suppose, which probably still internally uses a string buffer for a format call anyway) one way or another. It barely costs you anything to then check for a trailing newline then (what a write_fmt_line would eliminate checking). Even scanning and splitting on newline characters is probably dwarfed in cost by the actual formatting and writing.


There are existing ways to accomplish what you've communicated you want, that work quite well already. What you're proposing is that the language is changed impacting everyone else to marginally improve (if at all) one edge case you're dealing with, and insisting it be done the way you are proposing it should be done, and just dismissing proposed alternatives as missing the point.

I hope you can see how this negatively reflects back on the proposals you put forward. You may not intend to be, but it reads as standoffish (or even bad faith, depending on how (un)charitable the reader is feeling) on our side of the screen, as we lack the context that you have.


And just in general, I also would like to see the formatting machinery more thoroughly documented and specified. Some Write sinks should probably guarantee being zero-(extra)-copy, in that they don't use an internal buffer and just write each fragment written by fmt calls. Maybe more sinks should provide an inherent write_fmt so you don't have to import Write to write to them. Perhaps the implementation could be improved or made more configurable, or more could be exposed to the consumer so they have more ways to implement write_fmt than just dumping to another sink. The most meaningful small improvement I can think of would actually be documenting if fmt_arguments.fmt(w) is guaranteed to be equivalent to w.write_fmt(fmt_arguments) (modulo requisite (re)borrowing), or just that it writes the same content.

But all of this can (and should) be done without breaking the public API.

3 Likes

It'd still be good to have a new API for "line" writers. Well maybe "paragraph" writers would be more accurate. But sure, make it a guarantee that write_fmt is only called once.