@bytebuster effectively supports custom computation expressions, but I still thought I was demonstrating how to combine the State and Maybe monad into one.
In “traditional” languages, we have good support for compiling values, such as integers, but we run into problems when developing parsers (derived values from a binary stream are essentially parsed). For parsers, we would like to compose simple parser functions into more complex parser functions, but here the "traditional" languages often do not have good support.
In functional languages, functions are as common as values, and since values can be composed, it is obvious that functions can also be.
First we define the StreamReader function. A StreamReader takes a StreamPosition (stream + position) and creates an updated StreamPosition and StreamReaderResult (read value or failure).
type StreamReader<'T> = StreamReader of (StreamPosition -> StreamPosition*StreamReaderResult<'T>)
(This is the most important step.)
We like to compose simple StreamReader functions into more complex ones. A very important property that we want to preserve is that the layout operation is “closed” under StreamReader , which means that the result of the composition is a new StreamReader , which, in turn, can be created endlessly.
To read the image, we need to read the width and height, calculate the product and read the bytes. Something like that:
let readImage = reader { let! width = readInt32 let! height = readInt32 let! bytes = readBytes (width*height) return width, height, bytes }
Due to the close of the readImage composition, readImage is a StreamReader<int*int*byte[]> .
To compose a StreamReader , as described above, we need to define a calculation expression, but before we can do this, we need to define the Return and Bind operation for the StreamReader . Turns out Yield is good too.
module StreamReader = let Return v : StreamReader<'T> = StreamReader <| fun sp -> sp, (Success v) let Bind (StreamReader t) (fu : 'T -> StreamReader<'U>) : StreamReader<'U> = StreamReader <| fun sp -> let tsp, tr = t sp match tr with | Success tv -> let (StreamReader u) = fu tv u tsp | Failure tfs -> tsp, Failure tfs let Yield (ft : unit -> StreamReader<'T>) : StreamReader<'T> = StreamReader <| fun sp -> let (StreamReader t) = ft () t sp
Return is trivial because StreamReader should return the given value and not update StreamPosition .
Bind bit more complicated, but it describes how to compose two StreamReader functions into a new one. Bind launches the first StreamReader function and checks the result if it fails, otherwise it uses the StreamReader result to calculate the second StreamReader and runs it at the update stream position.
Yield simply creates a StreamReader function and runs it. Yield used by F # when building computation expressions.
Finally, create a calculation expression builder
type StreamReaderBuilder() = member x.Return v = StreamReader.Return v member x.Bind(t,fu) = StreamReader.Bind t fu member x.Yield(ft) = StreamReader.Yield ft let reader = StreamReaderBuilder ()
Now we have created the basic structure for combining the functions of StreamReader . In addition, we would need to define primitive StreamReader functions.
Full example:
open System open System.IO // The result of a stream reader operation is either // Success of value // Failure of list of failures type StreamReaderResult<'T> = | Success of 'T | Failure of (string*StreamPosition) list and StreamPosition = { Stream : byte[] Position : int } member x.Remaining = max 0 (x.Stream.Length - x.Position) member x.ReadBytes (size : int) : StreamPosition*StreamReaderResult<byte[]> = if x.Remaining < size then x, Failure ["EOS", x] else let nsp = StreamPosition.New x.Stream (x.Position + size) nsp, Success (x.Stream.[x.Position..(x.Position + size - 1)]) member x.Read (converter : byte[]*int -> 'T) : StreamPosition*StreamReaderResult<'T> = let size = sizeof<'T> if x.Remaining < size then x, Failure ["EOS", x] else let nsp = StreamPosition.New x.Stream (x.Position + size) nsp, Success (converter (x.Stream, x.Position)) static member New sp = {Stream = s; Position = p;} // Defining the StreamReader<'T> function is the most important decision // In this case a stream reader is a function that takes a StreamPosition // and produces a (potentially) new StreamPosition and a StreamReadeResult type StreamReader<'T> = StreamReader of (StreamPosition -> StreamPosition*StreamReaderResult<'T>) // Defining the StreamReader CE module StreamReader = let Return v : StreamReader<'T> = StreamReader <| fun sp -> sp, (Success v) let Bind (StreamReader t) (fu : 'T -> StreamReader<'U>) : StreamReader<'U> = StreamReader <| fun sp -> let tsp, tr = t sp match tr with | Success tv -> let (StreamReader u) = fu tv u tsp | Failure tfs -> tsp, Failure tfs let Yield (ft : unit -> StreamReader<'T>) : StreamReader<'T> = StreamReader <| fun sp -> let (StreamReader t) = ft () t sp type StreamReaderBuilder() = member x.Return v = StreamReader.Return v member x.Bind(t,fu) = StreamReader.Bind t fu member x.Yield(ft) = StreamReader.Yield ft let reader = StreamReaderBuilder () let read (StreamReader sr) (bytes : byte[]) (pos : int) : StreamReaderResult<'T> = let sp = StreamPosition.New bytes pos let _, sr = sr sp sr // Defining various stream reader functions let readValue (converter : byte[]*int -> 'T) : StreamReader<'T> = StreamReader <| fun sp -> sp.Read converter let readInt32 = readValue BitConverter.ToInt32 let readInt16 = readValue BitConverter.ToInt16 let readBytes size : StreamReader<byte[]> = StreamReader <| fun sp -> sp.ReadBytes size let readImage = reader { let! width = readInt32 let! height = readInt32 let! bytes = readBytes (width*height) return width, height, bytes } [<EntryPoint>] let main argv = // Sample byte stream let bytes = [|2;0;0;0;3;0;0;0;1;2;3;4;5;6|] |> Array.map byte let result = read readImage bytes 0 printfn "%A" result 0