I have the following code that try to read a possibly incomplete data (image data, for example) from a network stream using usual MaybeBuilder:
let image = maybe {
let pos = 2 //Initial position skips 2 bytes of packet ID
let! width, pos = readStreamAsInt 2 pos
let! height, pos = readStreamAsInt 2 pos
let! data, pos = readStream (width*height) pos
advanceInStream pos
return {width = width; height = height; pixels = data}
}
So, readStream[asInt] [numBytes] [offset] function returns Some [data] or None if data has not arrived yet in a NetworkStream. advanceInStream function is executed when whole network packet is read.
I wonder if there is some way to write some custom computation expression builder to hide pos passing from its user, since it's always the same - I read some data and position in stream and pass it to the next read function as a last parameter.
P.S. MaybeBuilder used:
type MaybeBuilder() =
member x.Bind(d,f) = Option.bind f d
member x.Return d = Some d
member x.ReturnFrom d = d
member x.Zero() = None
let maybe = new MaybeBuilder()
P.P.S
On second thought it seems I have to make pos mutable, because of possible "for" or "while" loops in reading. Simple let! works fine with pos Bind shadowing, but you can't hold onto immutability if you add reading in a loop, right? The task becomes trivial then.
@bytebuster is making good points of maintainability about custom computation expressions but I still thought I demonstrate how to combine the
State
andMaybe
monad into one.In "traditional" languages we have good support for composing values such as integers but we run into problems when developing parsers (Producing values from a binary stream is essentially parsing). For parsers we would like to compose simple parser functions into more complex parser functions but here "traditional" languages often lack good support.
In functional languages functions are as ordinary as values and since values can be composed obviously functions can be as well.
First let's define a
StreamReader
function. AStreamReader
takes aStreamPosition
(stream + position) and produces an updatedStreamPosition
and aStreamReaderResult
(the read value or a failure).(This is the most important step.)
We like to be able to compose simple
StreamReader
functions into more complex ones. A very important property we want to maintain is that the compose operation is "closed" underStreamReader
meaning that result of composition is a newStreamReader
which in turn can be composed endlessly.In order to read an image we need to read the width & height, compute the product and read the bytes. Something like this:
Because of composition being closed
readImage
is aStreamReader<int*int*byte[]>
.In order to be able to compose
StreamReader
like above we need to define a computation expression but before we can do that we need to define the operationReturn
andBind
forStreamReader
. It turns outYield
is good to have as well.Return
is trivial as theStreamReader
should return the given value and don't update theStreamPosition
.Bind
is a bit more challenging but describes how to compose twoStreamReader
functions into a new one.Bind
runs the firstStreamReader
function and checks the result, if it's a failure it returns a failure otherwise it uses theStreamReader
result to compute the secondStreamReader
and runs that on the update stream position.Yield
just creates theStreamReader
function and runs it.Yield
is used by F# when building computation expressions.Finally let's create the computation expression builder
Now we built the basic framework for combining
StreamReader
functions. In addition we would we need to define the primitiveStreamReader
functions.Full example: