-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Make IO#skip IO#write returns the number of bytes it skipped/written #9233
Make IO#skip IO#write returns the number of bytes it skipped/written #9233
Conversation
72b57bc
to
7da54a3
Compare
I added a tiny amount of specs for write. I am in doubt whether all write operations should return the byte counts or not.
It's a bit weird that write would return UInt64 but read Int32. I understand it is convenient, but it still feels odd. I think this is ready for a round of feedback before going further. |
7da54a3
to
4243c5e
Compare
Return bytes only in write_* operations. printf, puts, etc return Nil
cb3c2c3
to
65d41d1
Compare
Rebased and updated. I removed the wrapper and implement the accounting in the ByteFormat and Encoding. These are the only cases needed if we keep the printf, puts and other methods to return Nil. Making only the @carlhoerberg , how does this sound for your use cases? |
How's the performance for the nested byte accounting? |
@carlhoerberg not good. It adds an extra layer. We also check that in ruby printf/puts return Nil. I wanted to have a composable solution, hence I went on |
Maybe &+ should be used instead of +? I don't know if that's where the overhead comes from. Do we want the write to fail on overflow if exceeding UInt64 max? |
Well, it can't overflow. There is not way to write more that UInt64 bytes in those method. The overhead i refer was due to a IO wrapper, not the addition itself. |
Ok, let's leave the ByteCounter out then, performance is way more important here. Let's just return the bytes where it's easy and cheap. |
Why is the written length needed? What's a use case? |
If the nested write calls return the size, why a wrapper is needed? |
Also, what other |
There is also Note that |
@waj Oh, I see. I'm still curious which methods needed a |
Also a note again that in Go for example the return value is an |
It bugs me a little the inconsistency with read operations. But it bugs me more if it would be with copy that is a write operation :-). I think it's mainly low level so UInt64 is fine here. @asterite making printf return number of bytes required (or was easier) with the wrapper. The implementation of I changed the implementation to use |
Oh, I see. Yeah, for textual things maybe returning the number of bytes is not needed. In Go |
Is every |
In Go Since in Crystal if What would be needed here is for exceptions to capture how many bytes were written before the exception happened. That said, no use case has been provided for this change other than "avoiding to call size on the given slice instead of getting the value from the call". |
This is required for Crystal 0.35.0 (see crystal-lang/crystal#9233)
@didactic-drunk @HankAnderson that's not true, consider this: class CustomProtcol < IO
def write(slice)
bytes = 0_i64
bytes += @io.write "prefix".to_slice
bytes += @io.write slice
bytes += @io.write "suffix".to_slice
end
end more than struct ShortString
def initialize(@string)
end
def to_io(io)
io.write_byte @string.bytesize.to_u8
io.write(@string.to_slice) + sizeof(UInt8)
end
end
socket = TCPSocket
bytes_written = socket.write_bytes ShortString.new("foo")
bytes_written # 4, not 3 It vastly simplifies position accounting. |
I don't think a custom protocol should be an IO, really... you are using IO in a wrong way. For example if you print an object, or puts an object to that IO, you will get many prefixes and suffixes, which is probably not expected. If you want to track written slice size in the custom protocol (which is not an IO!) then do it, but all IOs just write the full slice as a byte size. For that matter, Flate, Gzip, etc. currently return the passed slice size, but in reality they might write less data (because it is compressed!). So already writer's return value is confusing. Is it returning how much data was actually written, or how much of the slice was written? If it's the latter, then your custom protocol should return the slice size, regardless of what else was written. If it's the former, then the stdlib is already broken. If you check what Go does, https://golang.org/pkg/io/#Writer (maybe you wanted this is Crystal because of Go or Ruby?) you'll notice that the return value is always in bounds with the passed slice size. It's returning how many bytes were written from the slice, not to the actual device (this is hard or impossible to know when data is buffered and compressed). So your custom protocol should return slice.size. In fact all IOs on crystal will return that because if they are successful, all the slice will be written out. If they are not, an exception will be raised. The problem is that this exception doesn't tell you how many bytes were written, and this is the only useful thing that we should add if we wanted to. But right now, IO#write return value is completely broken and nonsense. |
I agree. It's also completely lacking any documentation. In what use case would it not return |
yes, it was a bad example, i'm not implementing a protocol with a custom IO, but the write_bytes is correct, you can't know how many bytes that was actually written if write_bytes/to_io doesn't return the actual number of written written, with prefixes/compressed or whatever. The "problem" it solves is that you don't have to have to repeatedly call But I agree, the lack of enforcing is problematic. |
To me it's a nice to have, not a need to have, you can hack around it with manual position accounting. And its current form seems to cause a lot of confusion and is hard (impossible?) to enforce. |
Maybe you can show us your code to understand why you need to know the position at every point? |
Then those methods should return |
We can enforce that the write returns always the slice size if we change the design of IO and add But that will be half way. The There are some contracts that can't be enforced by types. We can discuss a design that is less fragile, but it can always be broken in some way by the implementation. I agree that protocols are better to be implemented by using and IO rather than inheriting one. But if we think of some transformation that can be activated or not to decorate an IO, inheriting make sense in the current state of the std-lib. If these decorators change the the amount of bytes written from the input maybe it is desired to still keep track of the underlying position count. I know is a maybe. But that is why I feel more comfortable with the proposed change. |
I think the changes on the
At the end we have something that makes programmers less happy without too much gain. Now, for the cases where having this value is actually useful, those could be solved in a different way:
I know going back and forward with changes like this is far from ideal. But since we're not at 1.0 yet it's a good opportunity to assume our mistakes and fix them before it's too late. |
Just for reference, because the original discussion about adding byte count to |
…written (crystal-lang#9233)" This reverts commit 7f13250.
Closes #9185
IO#skip
still allows any Int as the argument, but UInt64 is returnedIO#write_* : UInt64
IO#printf(...) : UInt64