3 ms·
This comment is kind of making my point. Unbuffered APIs are meant to be tricky: They ask for length (even in languages where arrays have a fixed length like J
by BoorishBears 5y ago
This comment is kind of making my point.
Unbuffered APIs are meant to be tricky: They ask for length (even in languages where arrays have a fixed length like Java) because the idea is:
- you're either building out a buffered wrapper and can't rely on the encoded length value (because your array length will be the entire buffer while you might only want to write part of it)
- or you want very precise control over the writes and are willing to risk those mistakes. So they need to open up that risk of a mistake.
It's an API that mirrors to the user how the underlying syscall operates, so the underlying complexity is being very plainly exposed.
-
Usually it's the buffered wrapper that will have a convenience method that takes a String or a full array without additional information, or there'll be an explicit call to force a buffered wrapper to use a 0-sized buffer.
C++: Without explicit call to setbuf, system defined default buffer used (matches POSIX behavior)
Java: FileWriter vs FileOutputStream, FileOutputStream is unbuffered will not take a string without manually encoding it to bytes first, FileWriter will take a string and is buffered
Python2: need explicit 0 sized buffer
Python3: need to flag as unbuffered and automatically converts file to binary mode, so calling write("SomeString") will not be allowed with unbuffered output.
C#: FileStream buffered by default, must explicitly ask for 0-size buffer
Obviously this won't hold across every language. But you can see that languages consistently make it easier to do the "right" thing in this case, while Rust isn't.
I'm not necessarily holding it against Rust either to be clear, but I'm holding it against the droves of comments here that are acting like this is such natural behavior you'd be a rookie to not catch it... it's been a loooong time since most people have used a language where that was "easy" behavior to uncover accidentally. Like I said even C++ went out of it's way to improve things in this regard.
- spoiler 5y agoRight, I see. I guess I understand where you're coming from (basically your expectations are shaped by other languages). That's fair enough, and I've had some of my expectations broken fore the same reasons in Rust, and it was around ergonomics too. Rust's solution to the problem, even if a departure from traditional solutions, is more ergonomic, and I like that. There's no reason buffered or buffered IO needs to be trickier than the other! Why sacrifice one? Sure you could have Write buffered by default and have UnbufferedWrite instead, but I personally find this "reversed API" idea really weird. It seems like a subtractive API rather than an additive one, and it also doesn't compose as nicely. Your argument about size proves my point too, so we're going in circles I guess. I'll summarise why I prefer Rust's approach (sans ergonomics): it's harder to introduce logic errors if all the information can be inferred from the value itself. If I want to send "hello", and somewhere I fucked up, so I'm thinking 4 bytes instead of 5 and now I sent "hell", and threw 'o' to the RAMs. Why? I can just tell it I wanna send "hello", and it does it. It's more robust. If I wanted to send just "hell" (assuming it's bytes) in I can just create a slice to the necessary data and pass that. The slice explicitly itself signals to other devs I wanna send a subset of the buffer, too. Doing it this way, buffered or unbuffered doesn't really matter: The API stays the same. Adding buffering is usually a one line change of converting your writer to a buffered writer. Someone called Rust's approach an "arcane incantation", if anything the only magical things here is the special/magic sized buffers or numbers in other languages you highlighted. Furthermore, I think half of the existing implementations probably didn't even think about this design decision at all, they just inherited it. At the end of the day, this discussion about buffering isn't super important in the grand scheme of things. The stdlib gives you just building blocks (Rust has great ones, IMO) to build a networking library you want, so your (un)buffering is going to be tucked away in a couples of places and you'll be using your own `report` or `send` or `set` methods instead of a sprinklings low-level write/read syscals everywhere anyway.
- Measter 5y agoThe `write` function here comes from the `Write` trait[0]. This isn't the unbuffered write API, it's just the write API. What it's writing to could be a `File`, or it could be `StdOut` or a `Vec<u8>`. It also doesn't take a string; it takes a slice of bytes. In Rust, prefixing a string literal makes it a `&[u8]` instead of a `&str`, which is why you can just pass it straight into `write`. Adding a length field would only make it more error-prone to use without any added benefit. Slices already know their length, and subslicing is trivial. If you have a larger slice, and you only want to write a subsection of it, you just index it. Forcing the user to pass in two lengths is just silly, and all the API implementations would do is subslice the input to that second length anyway. You could argue that `File::open` and `File::create` should return a `BufReader<File>` and `BufWriter<File>` respectively, and I would agree that this would be reasonable as the most common use-case probably benefits from buffered IO. However, that ship's already sailed, and those functions can't be changed. The description for the `File` type should also more explicitly state that IO is unbuffered. I'm surprised it doesn't, the docs are typically better at that sort of thing. [0] https://doc.rust-lang.org/std/io/trait.Write.html https://doc.rust-lang.org/std/io/trait.Write.html
- BoorishBears 5y agoIt was never lost on me that the length is encoded, my very first comment mentions it's the same in Java: the number of bytes you pass in is encoded. My point isn't that every language should force that syntax ('offset' for example, doesn't make sense in languages with pointer math for example) Rather the point is that when the language doesn't follow that convention, it should buffer by default. I guess it is a blind spot in the standard library then