-
-
Notifications
You must be signed in to change notification settings - Fork 144
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Improve WriteBytesExt API #27
Comments
Alternatively one could elevate the enum variants to real types and use them as parameters which allows a more ergonomic use (as it should make it easier to write generic code for it) at the cost of being as long as before.
Then
would become
|
I've heard a few people moan about the current API, but this is the best alternative I've seen so far. A few comments:
I want to noodle on this some more. I'd also love to get feedback from others. I don't have a good handle on who is using cc @TyOverby @jamesrhurst @blackbeam @sfackler @mitsuhiko @cyderize |
I use bincode::encode(&my_struct); // default to network byte order
bincode::encode::<LittleEndian>(&my_struct); // specify endianness
// Use other endianness like above The benefit to the way that @BurntSushi currently implements it is that having these functions called with different endianness imposes no runtime lookup, which is very important to bincode. |
@TyOverby This is the first I'm hearing about default types for functions. Cool! Is there an RFC? |
My use case is pretty simple - I never need to generically vary over either endianness or the type. Either the |
@BurntSushi Yeah, the main default types RFC. Their example is pub fn range<A:Enumerable=uint>(start: A, stop: A) -> Range<A> {
Range{state: start, stop: stop, one: One::one()}
} |
I must redact my criticism a little bit. :( I do not know how I missed that but That means my strong arguments shrink to the return type polymorphism. Still I think a shortcut like write_le would be nice. |
Have you spend some more thought about it? If one would use a generic solution, third party types would be able to hook into the same infrastructure by simply implementing ByteOrder. Something like this: #![feature(associated_consts)]
use std::io;
trait ByteOrder<T> {
const Size: usize;
fn read_bytes(buf: &[u8]) -> T;
}
enum LittleEndian {}
enum BigEndian {}
impl ByteOrder<u8> for LittleEndian {
const Size: usize = 1;
fn read_bytes(buf: &[u8]) -> u8 {
buf[0]
}
}
impl ByteOrder<u16> for LittleEndian {
const Size: usize = 2;
fn read_bytes(buf: &[u8]) -> u16 {
((buf[1] as u16) << 8) | buf[0] as u16
}
}
trait ReadBytesExt<T> {
fn read<B: ByteOrder<T>>(&mut self) -> io::Result<T>;
}
impl<T, R> ReadBytesExt<T> for R
where R: io::Read {
fn read<B: ByteOrder<T>>(&mut self) -> io::Result<T> {
let mut buf = [0; B::Size];
assert!(try!(self.read(&mut buf)) == B::Size);
Ok(B::read_bytes(&buf))
}
}
fn main() {
let mut v = &[1, 2, 3][..];
let a: u8 = v.read::<LittleEndian>().unwrap();
let b: u16 = v.read::<LittleEndian>().unwrap();
println!("{} {}", a, b);
} (Doesn’t compile thought. Probably one has to wait for non-type type parameters) Edit: Seems like it is just a temporary restriction that the code gets rejected. |
Ok I just wrote the whole thing to illustrate my point: nwin/byteio. It has almost the same API as byteorder: use byteio::{LittleEndian, ReadBytesExt};
let mut reader = &[1, 2][..];
let val: u16 = reader.read_as::<LittleEndian>().unwrap();
assert_eq!(val, 513u16); Benchmarks indicate that it may be faster than byteorder but I don’t trust them. In theory it should, since I’m using byte arrays instead of slices. |
@nwin Sorry for the late reply. I do actually like your approach. Not only is the implementation smaller/simpler, but the API surface is smaller too. What is the best way to proceed? Lots of people are using
|
I agree, that seems to be a good approach. What about the method name of Read/WriteBytesExt? |
I really like how |
I’ll file a PR this weekend then we can discuss further issues with the new implementation there. |
I'd like to present a different opinion: having the type in the method signature is a good thing, even if it's redundant. I've followed for a while the cleanups the LibreOffice team were applying to the legacy OpenOffice codebase. One of these cleanups was to change the implicit SvStream operators, where writing a 16-bit quantity would be something like:
to an explicit method call, like:
This prevents a kind of error they often encountered, where changing a variable type somewhere accidentally changed a binary serialization format elsewhere. Since a byteorder crate is going to be used for binary serialization, where the type width is as important as its endianness, that's important. The API I'd prefer is something like:
(same for _be/_ne), where the type can be elided if the variable is declared nearby, but can also be given explicitly to prevent accidental serialization format changes (in this example, the compiler would give an error if |
That is a good point. In that case
would be a good API. Unfortunately the type can’t be elided in that call (yet?). |
If you want to ever be able to elide it, it must be in the opposite order:
So the declaration would be something like:
The question would then be: how to default the type parameter?
The obvious answer would be to add a
but that looks awfully circular. |
In the worst case, the explicit version could be written on top of the implicit version, as a separate method:
but of course having a single method would be much cleaner. And there's always the following, which should already work with your current code:
I could also imagine a type
|
Any updates on this? Many ideas in the comments above look a lot better than the current design... |
@LukasKalbertodt Not yet, just haven't gotten to it yet, but I haven't forgotten. One excuse I have is that there are many reverse dependencies of |
@BurntSushi Do you plan on rewriting byteorder from the ground up with the new api, or just adding these on top? I still plan on using the generic functions heavily; will I be forced to use an abandoned version? |
@TyOverby I forgot about that. There's no way I'm maintaining two distinct APIs. If the new API loses a real use case, then that kind of seems like a serious mark against it. |
Yeah, I think that once default-types for functions land, having trait-based (compile-time) dispatch will be a lot more attractive. With the same function you could express all of these some library API. // call regularly
function_that_uses_byteorder(&my_obj, &mut writer);
// call with specified order
function_that_uses_byteorder::<LittleEndian>(&my_obj, &mut writer);
// all I care about is perf!
function_that_uses_byteorder::<NativeEndian>(&my_obj, &mut writer); This is exactly what I plan on doing for Bincode. Right now I use big-endian, but for projects like Servo that are just using Bincode for cross-process communication, I could let them choose the endianness without convoluting the Bincode API. |
@TyOverby So it sounds like we might want to table API redesign (if any) until at least default-types for functions lands. |
I did not read all comments above, so I just have to agree that waiting for Rust features that allow a better API design is probably a good idea. And I also agree that maintaining two different APIs is not a good idea. Thanks for the status update :) |
I count 50 crates with a
I don't really care if it's their "own fault" or not. The fact is, there are lots of crates that will break immediately for all downstream users. If there's an opportunity to mitigate that, then I'm going to take it. And also, the README for this project recommended a
It will mitigate breakage on active crates. |
Oops -- shame on me. Nevermind then. |
It seems this issue is abandoned for now. So, there are two ways out of here: change api with backward compatibility (via semver) or fork this crate with proposed changes. |
It is not abandoned, as far as I understand we're just waiting for default type parameters to happen (rust-lang/rust#27336). At least the |
1.2 years of waiting? Really perfect issue resolving way for software engineers. |
I'm planning to take this crate to 1.0 soon with the current API. If and when default type parameters is a thing, then we can revisit. |
At the moment the byteorder crate uses a enum to generalize over the endianness and the method name to select the type. Both is not very ergonomic to use. I propose the following interface:
First of all it gets rid of the enum. Since the enum is purely a compile time parameter it cannot be used for dynamic dispatch. This is as good or bad as having it directly the method name. Thus I do not see the point of having it. Secondly it gets rid of the redundant type name in the signature.
This shortens the method call significantly
becomes
My two points are:
*BytesExt
traits cannot be use to write generic code that abstracts over endianness. Again no benefit for the user.The text was updated successfully, but these errors were encountered: