F # evaluate expression transparent state passing with bind

I have the following code that tries to read possibly incomplete data (for example, image data) from a network stream using a regular 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, the readStream [asInt] [numBytes] [offset] function returns some [data] or None if the data has not yet been received in NetworkStream. The advanceInStream function is executed when the entire network packet is read.

I wonder if there is any way to write some kind of custom expression expression builder to hide the point sending from his user, since he is always the same - I read some data and position in the stream and passed it to the next read function as last parameter.

PS MaybeBuilder:

 type MaybeBuilder() = member x.Bind(d,f) = Option.bind fd member x.Return d = Some d member x.ReturnFrom d = d member x.Zero() = None let maybe = new MaybeBuilder() 

PPS

On the other hand, it seems that I had to make pos mutable, due to possible for or while loops when reading. Just let it go! works great with pos Binds shadows, but you can't hold on to immutability if you add reading to the loop, right? Then the task becomes trivial.

+6
source share
1 answer

@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 
+3
source

Source: https://habr.com/ru/post/989059/


All Articles