Why ReadWriteStream>>#contents?

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
7 messages Options
Reply | Threaded
Open this post in threaded view
|

Why ReadWriteStream>>#contents?

Chris Muller-4
Does anyone know why ReadWriteStream overrides #contents from WriteStream?

WriteStream behaves as I would expect

   |stream| stream := WriteStream on: String new.
   stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->  'C'   as expected"

but ReadWriteStream doesn't...

   |stream| stream := ReadWriteStream on: String new.
   stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "---> 'Chris'   unexpected!"

I want to reuse a ReadWriteStream, so I want #contents to honor the end position.  What's going on here?


cbc
Reply | Threaded
Open this post in threaded view
|

Re: Why ReadWriteStream>>#contents?

cbc
I suspect that is because ReadWriteStream is built to work roughly like FileStreams- except over other arbitrary collections.  Basically, let's you read at arbitrary spots, go back, overwrite those spots, and then continue on.  As such, I would think #contents is exactly the right behavior.  Take this example:

|stream| stream := ReadWriteStream on: 'Chirs'.
stream next; contents.  "---< 'Chris' - the contents of the underlying collection, not some adhoc part."

What you are probably looking for is #resetToStart (which if you used in your example would not have surprised you - although the name surprised me).

-cbc

On Tue, May 1, 2018 at 1:00 PM, Chris Muller <[hidden email]> wrote:
Does anyone know why ReadWriteStream overrides #contents from WriteStream?

WriteStream behaves as I would expect

   |stream| stream := WriteStream on: String new.
   stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->  'C'   as expected"

but ReadWriteStream doesn't...

   |stream| stream := ReadWriteStream on: String new.
   stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "---> 'Chris'   unexpected!"

I want to reuse a ReadWriteStream, so I want #contents to honor the end position.  What's going on here?






Reply | Threaded
Open this post in threaded view
|

Re: Why ReadWriteStream>>#contents?

Nicolas Cellier
In reply to this post by Chris Muller-4
IMO ReadWriteStream is an awfull mess and should better not be used at all.

2018-05-01 22:00 GMT+02:00 Chris Muller <[hidden email]>:
Does anyone know why ReadWriteStream overrides #contents from WriteStream?

WriteStream behaves as I would expect

   |stream| stream := WriteStream on: String new.
   stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->  'C'   as expected"

but ReadWriteStream doesn't...

   |stream| stream := ReadWriteStream on: String new.
   stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "---> 'Chris'   unexpected!"

I want to reuse a ReadWriteStream, so I want #contents to honor the end position.  What's going on here?






Reply | Threaded
Open this post in threaded view
|

Re: Why ReadWriteStream>>#contents?

Chris Muller-3
C'mon, it has 9 tiny, concise methods, (15 including extensions from
EToys and Compression) and so only Tim is allowed to characterize that
as an "awful mess".  :)   No seriously, being the superclass for
FileStream, it handles all of Squeak's file contents processing and works well.

I guess Chris' explanation is a reasonable explanation for such glaring
inconsistencies in behavior between superclass and subclass, so I
ended up adding my own #content method which does what I want...



On Tue, May 1, 2018 at 3:21 PM, Nicolas Cellier
<[hidden email]> wrote:

> IMO ReadWriteStream is an awfull mess and should better not be used at all.
>
> 2018-05-01 22:00 GMT+02:00 Chris Muller <[hidden email]>:
>>
>> Does anyone know why ReadWriteStream overrides #contents from WriteStream?
>>
>> WriteStream behaves as I would expect
>>
>>    |stream| stream := WriteStream on: String new.
>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>> 'C'   as expected"
>>
>> but ReadWriteStream doesn't...
>>
>>    |stream| stream := ReadWriteStream on: String new.
>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>> 'Chris'   unexpected!"
>>
>> I want to reuse a ReadWriteStream, so I want #contents to honor the end
>> position.  What's going on here?
>>
>>
>>
>
>
>
>

Reply | Threaded
Open this post in threaded view
|

Re: Why ReadWriteStream>>#contents?

Chris Muller-3
It seems I've asked this question before:

   http://forum.world.st/surprising-behavior-from-MultiByteBinaryOrTextStream-gt-gt-contents-td4706322.htmlI

This made me remember some of the really great points and discussion
about the Stream design you've made in the past.  This, for example:

   http://forum.world.st/In-memory-FileSystem-write-streams-not-being-polymorphic-td4721006i20.html

Good to re-read that thread from time to time..


On Tue, May 1, 2018 at 5:16 PM, Chris Muller <[hidden email]> wrote:

> C'mon, it has 9 tiny, concise methods, (15 including extensions from
> EToys and Compression) and so only Tim is allowed to characterize that
> as an "awful mess".  :)   No seriously, being the superclass for
> FileStream, it handles all of Squeak's file contents processing and works well.
>
> I guess Chris' explanation is a reasonable explanation for such glaring
> inconsistencies in behavior between superclass and subclass, so I
> ended up adding my own #content method which does what I want...
>
>
>
> On Tue, May 1, 2018 at 3:21 PM, Nicolas Cellier
> <[hidden email]> wrote:
>> IMO ReadWriteStream is an awfull mess and should better not be used at all.
>>
>> 2018-05-01 22:00 GMT+02:00 Chris Muller <[hidden email]>:
>>>
>>> Does anyone know why ReadWriteStream overrides #contents from WriteStream?
>>>
>>> WriteStream behaves as I would expect
>>>
>>>    |stream| stream := WriteStream on: String new.
>>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>>> 'C'   as expected"
>>>
>>> but ReadWriteStream doesn't...
>>>
>>>    |stream| stream := ReadWriteStream on: String new.
>>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>>> 'Chris'   unexpected!"
>>>
>>> I want to reuse a ReadWriteStream, so I want #contents to honor the end
>>> position.  What's going on here?
>>>
>>>
>>>
>>
>>
>>
>>

Reply | Threaded
Open this post in threaded view
|

Re: Why ReadWriteStream>>#contents?

Nicolas Cellier
In reply to this post by Chris Muller-3
Hi Chris,
Yes, you are right, ReadWriteStream looks simple. But it is not.
It inherits from WriteStream which is a Stream that cannot read (self shouldNotImplement).
WriteStream inherits itself from PositionableStream which can neither read nor write (self subclassResponsibility).
PositionableStream is rather read-oriented though, see inst.var. readLimit and its usage which is clearly for delimiting read limit (thus the name).

WriteStream reuse readLimit with a slightly different purpose: indicate the right-most position written so far.
The purpose is to answer the whole contents even of position has been moved backward (rather than truncate to current position).
Hence, the natural invariant should be this one:
the readLimit should be incremented when writing pastEnd (past the right-most position written so far - readLimit).
But it is not... Indeed, primitive: 66 (nextPut:) increments position and ignores the readLimit, and just consider the writeLimit:
    rw := ReadWriteStream on: (String new: 10).
    rw nextPut: $a.
    rw instVarNamed: #readLimit
PastEnd is interpreted as the physical limit - writeLimit - so as to have efficient primitives as long as there is room in the target collection.

That doesn't matter, because the position is shared for reading and writing operations.
Thanks to this property, we can to enforce the invariant differently:
hack every place where we might assign a position backward with a:
    readLimit := readLimit max: position.

The hack is both clever and fragile...
It's a nice hack, because as you observed, there are not so many methods requiring an overwrite...
But it's fragile, because it means necessary chirurgical operations in subclasses to maintain the invariant.
And if ever you want to subclass with a Stream maintaining two separate positions for read and write, boom!
Since this invariant isn't documented anywhere, probably because you just have to read the code :(
our best way to learn it will be pain: probably a feedback loop valued by the biological analogy ;)

And heavy chirurgy is what happens in the subclasses zoo which are further hacked...
The inst. var. position is no more the absolute position in underlying collection, but rather a relative position in some buffer/segment, both in case of StandardFileStream and CompressedSourceStream.
So these classes also modify the meaning of readLimit inst. var. to be the number of bytes readable in the buffer.
And they need yet another way to access the absolute readLimit (endOfFile for CompressedSourceStream, and OS fseek-ftell-based for StandardFileStream).

Now, if you are about to modify one of these classes, and try and track usage of position/readLimit inst. var. you are in brain trouble.
Remember that position and self position might be different things...

So you are somehow right, ReadWriteStream is not at the level of awfullness I described, but it carries the seeds for this awfullness to be further developped.
Reusing inst. var. with a different intention across hierarchy is a good recipe for brain storm (a bug factory and a limitation to extensibility).
Most of the time, we don't need interleaved read/writes, except for a database backend or a few other stateful cases.
Instead, we mostly write then read. I already replaced a few instances of ReadWriteStream on this YAGNI principle, and would like this work to be continued, thus my simplistic reaction.
Of course, your case might differ.

Nicolas

2018-05-02 0:16 GMT+02:00 Chris Muller <[hidden email]>:
C'mon, it has 9 tiny, concise methods, (15 including extensions from
EToys and Compression) and so only Tim is allowed to characterize that
as an "awful mess".  :)   No seriously, being the superclass for
FileStream, it handles all of Squeak's file contents processing and works well.

I guess Chris' explanation is a reasonable explanation for such glaring
inconsistencies in behavior between superclass and subclass, so I
ended up adding my own #content method which does what I want...



On Tue, May 1, 2018 at 3:21 PM, Nicolas Cellier
<[hidden email]> wrote:
> IMO ReadWriteStream is an awfull mess and should better not be used at all.
>
> 2018-05-01 22:00 GMT+02:00 Chris Muller <[hidden email]>:
>>
>> Does anyone know why ReadWriteStream overrides #contents from WriteStream?
>>
>> WriteStream behaves as I would expect
>>
>>    |stream| stream := WriteStream on: String new.
>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>> 'C'   as expected"
>>
>> but ReadWriteStream doesn't...
>>
>>    |stream| stream := ReadWriteStream on: String new.
>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>> 'Chris'   unexpected!"
>>
>> I want to reuse a ReadWriteStream, so I want #contents to honor the end
>> position.  What's going on here?
>>
>>
>>
>
>
>
>




Reply | Threaded
Open this post in threaded view
|

Re: Why ReadWriteStream>>#contents?

Chris Muller-4
Thanks for that fantastic explanation, Nicolas.  If I were to try to distill your point, it's that each class in Squeak's legacy Stream hierarchy has its own view on how ITS concrete instances interpret the meaning of the API.  And while, thankfully, they're mostly consistent with #next, #peek, and #nextPut:, they're not at all on other messages like #contents.  The result of this is that the classes are mostly silo'ed from each other because it's complex to try to start sharing the behaviors.  That was my take-away, anyway, please let me know if I missed it.

One thing I'm not sure about is whether you feel commingling read ability with write ability in the same object is a bad thing in an abstract sense?  Or, it just didn't get implemented via a well-planned start in Squeak's case (organic growth), and so it's furthering dependencies on something that is hacked together and hard to change.

You mentioned the database exception, and sometimes it's hard for me to think outside of that context, so ReadWriteStream feels like a natural encapsulation of behaviors for accessing and updating file contents.  If there's any inherent advantage to splitting the core streaming capabilities (reading, writing and positioning) into separate classes, or whether all those should be the API of all streams (but some may have to delete positioning via #shouldNotImplement).  That would leave the class-hierarchy abstractions to focus on the data itself for things like conversion, compression, encryption, etc. instead of differing stream core capabilities...

Regards,
  Chris

On Wed, May 2, 2018 at 7:14 AM, Nicolas Cellier <[hidden email]> wrote:
Hi Chris,
Yes, you are right, ReadWriteStream looks simple. But it is not.
It inherits from WriteStream which is a Stream that cannot read (self shouldNotImplement).
WriteStream inherits itself from PositionableStream which can neither read nor write (self subclassResponsibility).
PositionableStream is rather read-oriented though, see inst.var. readLimit and its usage which is clearly for delimiting read limit (thus the name).

WriteStream reuse readLimit with a slightly different purpose: indicate the right-most position written so far.
The purpose is to answer the whole contents even of position has been moved backward (rather than truncate to current position).
Hence, the natural invariant should be this one:
the readLimit should be incremented when writing pastEnd (past the right-most position written so far - readLimit).
But it is not... Indeed, primitive: 66 (nextPut:) increments position and ignores the readLimit, and just consider the writeLimit:
    rw := ReadWriteStream on: (String new: 10).
    rw nextPut: $a.
    rw instVarNamed: #readLimit
PastEnd is interpreted as the physical limit - writeLimit - so as to have efficient primitives as long as there is room in the target collection.

That doesn't matter, because the position is shared for reading and writing operations.
Thanks to this property, we can to enforce the invariant differently:
hack every place where we might assign a position backward with a:
    readLimit := readLimit max: position.

The hack is both clever and fragile...
It's a nice hack, because as you observed, there are not so many methods requiring an overwrite...
But it's fragile, because it means necessary chirurgical operations in subclasses to maintain the invariant.
And if ever you want to subclass with a Stream maintaining two separate positions for read and write, boom!
Since this invariant isn't documented anywhere, probably because you just have to read the code :(
our best way to learn it will be pain: probably a feedback loop valued by the biological analogy ;)

And heavy chirurgy is what happens in the subclasses zoo which are further hacked...
The inst. var. position is no more the absolute position in underlying collection, but rather a relative position in some buffer/segment, both in case of StandardFileStream and CompressedSourceStream.
So these classes also modify the meaning of readLimit inst. var. to be the number of bytes readable in the buffer.
And they need yet another way to access the absolute readLimit (endOfFile for CompressedSourceStream, and OS fseek-ftell-based for StandardFileStream).

Now, if you are about to modify one of these classes, and try and track usage of position/readLimit inst. var. you are in brain trouble.
Remember that position and self position might be different things...

So you are somehow right, ReadWriteStream is not at the level of awfullness I described, but it carries the seeds for this awfullness to be further developped.
Reusing inst. var. with a different intention across hierarchy is a good recipe for brain storm (a bug factory and a limitation to extensibility).
Most of the time, we don't need interleaved read/writes, except for a database backend or a few other stateful cases.
Instead, we mostly write then read. I already replaced a few instances of ReadWriteStream on this YAGNI principle, and would like this work to be continued, thus my simplistic reaction.
Of course, your case might differ.

Nicolas

2018-05-02 0:16 GMT+02:00 Chris Muller <[hidden email]>:
C'mon, it has 9 tiny, concise methods, (15 including extensions from
EToys and Compression) and so only Tim is allowed to characterize that
as an "awful mess".  :)   No seriously, being the superclass for
FileStream, it handles all of Squeak's file contents processing and works well.

I guess Chris' explanation is a reasonable explanation for such glaring
inconsistencies in behavior between superclass and subclass, so I
ended up adding my own #content method which does what I want...



On Tue, May 1, 2018 at 3:21 PM, Nicolas Cellier
<[hidden email]> wrote:
> IMO ReadWriteStream is an awfull mess and should better not be used at all.

>
> 2018-05-01 22:00 GMT+02:00 Chris Muller <[hidden email]>:
>>
>> Does anyone know why ReadWriteStream overrides #contents from WriteStream?
>>
>> WriteStream behaves as I would expect
>>
>>    |stream| stream := WriteStream on: String new.
>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>> 'C'   as expected"
>>
>> but ReadWriteStream doesn't...
>>
>>    |stream| stream := ReadWriteStream on: String new.
>>    stream nextPutAll: 'chris'; reset; nextPutAll: 'C'; contents     "--->
>> 'Chris'   unexpected!"
>>
>> I want to reuse a ReadWriteStream, so I want #contents to honor the end
>> position.  What's going on here?
>>
>>
>>
>
>
>
>