If you have a formatting operation that produces an in-memory string, you cannot use it if the amount of text being produced is too large, or if you do not know if it is too large. But if it is not too large, you can then output the string to a buffered output file, although this is less efficient because it involves an extra copy and potentially dirties a lot more memory.
So these two ways of handling formatting are easily interconvertible when memory permits, but in some circumstances only the first one is applicable, and it is always more efficient.
Therefore, when reliability or efficiency is important, generating a formatted string should be implemented as a layer on top of sending formatted output to a stream, not vice versa.
There are cases where reliability and efficiency are not important, because your code is being used by end users and not as part of a library, and an error message is also an acceptable result of trying to run your program, and in those cases you should probably write your program in a language like Lua, Python, or JS.
It is probably true that if you are used to languages like those, a lot of design decisions that are necessary for reliability and efficiency will be counterintuitive to you.
This seems like an extremely rare case, which is itself a subset of the very rare case that you are formatting something that can be arbitrarily large. Oh, and the programmer must somehow have overlook this and not written it formatted output to output in reasonably-sized chunks.
Meanwhile, having back-and-forth between the formatter and I/O means the formatter must correctly handles all the I/O problems and corner cases: disk full, disconnect, transient failures, ... not even going into that for some of them, you might want the caller to decide the outcome, which is easy when you split formatting and I/O, but hard when the formatting performs the I/O.
I think this is a case that it may sounds superior to mix both, but it is actually buying next to nothing but both complicates matters and probably hides or ignores real issues.
It takes a minimum of twice as much memory to store the original data and its formatted stream. Some systems don't have much memory, and some systems work with large amounts of data. Some systems work with more data than fits in RAM, or more data than even fits on disk, or even infinite streams of data, from sensors or generated data. All of these use-cases must be accommodated by Hare.
That said, you can easily format into a dynamically allocated string in Hare if you so desire:
let s = fmt::asprintf("Hello, {}!", user);
defer free(s);
This has the obvious downside of requiring an additional memory allocation, which can also fail just as easily as I/O can, and must be freed after use. However, this function does not require I/O, so all of the I/O failure cases are ruled out. There are trade-offs between these two approaches. Hare's design is meant to let the user evaluate these trade-offs for their use-case and make an explicit, planned decision regarding their needs and potential failure modes.Incidentally this is one of the things that the limited form of laziness found in Python generators could help with, if Hare had it; you can think of an input stream as a lazy string. Unlike full laziness, generators have very predictable memory usage.
fmt::println(x, y, z);
instead of (also untested) fmt::fprintf(os::stdout, "{}{}{}\n", x, y, z);
? I mean I agree that presumably anything you could do in the first form could be done in the second form, less conveniently as you say, but if you omit println from fmt then all the Golang programmers learning Hare will be surprised about the missing stair they expect as the Hare counterpart to fmt.Println. And I think omitting the terminating newline in your format string is a common enough bug that it's probably worthwhile to include a separate function that adds it implicitly, especially in the case of fmt::print, which doesn't have a format string to add it to.Even without Golang experience, if you're going to have an fprintf and a println, I'd look for the println in the module that has the fprintf in it.
Also not all streams are equal: stdio is heavier than file streams, which in turn are heavier than memory-backed streams (for example, stdio will probably need a built-in lock while string streams needn't). If you only need for example memory-backed streams you want to never see any code related to stdio, which might not be possible in some designs.
Why is io::handle a separate type from io::stream? Is it just an efficiency hack to avoid an indirection through a function pointer for the common case where what you're writing to actually is a file? Can't io::handle eliminate the entanglement between buffered output and users of streams like the old io::println? I feel like there's something I'm not understanding here. (Is fd: int really the right way to handle a pointer to a structure representing how data is being transparently compressed into a zipfile, or a terminal emulator state that is being updated by writing bytes to it?)
io::handle is separate from io::stream because it needs to store either a stream or an io::file. io::file is necessary because it's required for many operating system constructs - a TCP socket is an io::file and cannot be an io::stream, for instance. Only io::files can be passed into syscalls like poll(2). However, stream is separately useful for building userspace I/O primitives, such as io::tee, cryptographic hashes and streams, and so on. So io::handle allows you to have an I/O object which is either a file descriptor (io::file) or a userspace stream (io::stream), but your code doesn't have to care about which it is.
I do understand why io::file needs to be separate from io::stream. (The io documentation introduction gives an explanation of what you're explaining above; I'd additionally offer the examples of a gzip stream, a UTF-8-decoded stream on an ISO-8859-1 text file, and maybe a stream that feeds into in-process terminal emulator logic.) I was asking why io::handle does. If you're writing code that takes an io::handle, generally the code cannot rely on the fact that io::file can be used for select() or ioctl() or getpeername(); if it needed to do that, you would have written it to take an io::file, not an io::handle. So, if your code is only going to invoke io::stream-like operations on the io::handle, it would be simpler if its argument were an io::stream instead of an io::handle.
But what if you want to give it an io::file? Well, it's easy enough to wrap an io::file in an io::stream that just invokes the appropriate rt operations, and in most languages the only per-call cost of doing that is that your function call is indirect (mov 12(%ebx), %ecx; call %ecx) rather than direct (call Zn#]io31337write). In fact, most code would be more* efficient that way, because right now if someone gives you an io::handle and you want to read from it, you're usually going to call io::read on it, adding an extra level of function call around the indirect call, because that's simpler than duplicating io::read's conditional call to rt::read in your own code. But dynamically most read and write calls will probably be on a bufio::bufstream (or some other io::stream), so io::read is just going to call .reader().
Does io::handle maybe exist only as an optimization for sendfile()?
Maybe it's too late for such changes, given the amount of existing Hare code, and this qualifies as bikeshedding, and if so, I apologize.
io::file exists for where it's needed to interface with the host system. io::stream is needed for userspace streams. io::handle is needed so that code which just wants to do I/O but doesn't care which of these two paradigms is in use can be useful for both.
I don't remember if Golang "stdio" has built-in locks. Hare doesn't seem to have threads or locks, o that may be a non-issue.