I am fairly new to F# but have been reading about workflows and computation expressions. From the reading I have done, I thought I felt I had at least a basic understanding of the purpose of a workflow and the syntax for using a workflow. Then, I came across an example here: BitWorker Workflow.
I tried copying the example code to a local project and ran it successfully. I started moving things around and getting a feel for what the code did, but I am still having trouble understanding how this workflow actually works. I haven't seen other examples where the workflow syntax was like: do bitWriter stream {...
instead of do workflow {...
.
let stream = new IO.MemoryStream()
// write TCP headers
do bitWriter stream {
do! BitWriter.WriteInt16(12345s) // source port
do! BitWriter.WriteInt16(12321s) // destination port
do! BitWriter.WriteInt32(1) // sequence number
do! BitWriter.WriteInt32(1) // ack number
do! BitWriter.WriteInt32(2, numBits = 4) // data offset
do! BitWriter.WriteInt32(0, numBits = 3) // reserved
}
I wouldn't have expected stream
to be a part of the bitWriter
workflow. What does the use of stream
here mean for the workflow?
This should be easier to explain if we look at a minimal example that implements a workflow like this. First, I'll define a type for operations that you can do. For simplicity, let's have just one:
type Operation =
| WriteInt32 of int
Typical F# computation builders do not take any constructor parameters, but you can actually take parameters - the computation builder here takes a stream as a parameter and creates a StreamWriter
. In the Bind
operation, the argument is one of our Operation
values and we handle that by writing the value to the stream writer. Then we just call the rest of the computation using f ()
type BitWriter(stream:IO.Stream) =
let wr = new IO.StreamWriter(stream)
member x.Bind(op, f) =
match op with
| WriteInt32 i -> wr.Write(i)
f ()
member x.Zero() = ()
member x.Run( () ) = wr.Dispose()
The Zero
and Run
operations are not particularly interesting, but Zero
is required by the translation and Run
lets us dispose of the writer. This is not the most idiomatic way of definining computation expressions - it does not follow the monadic structure - but it actually works! Two helpers before we can use it:
let writeInt32 i = WriteInt32 i
let bitWriter stream = BitWriter(stream)
And now you can write code that is pretty much what the above library does:
let stream = new IO.MemoryStream()
bitWriter stream {
do! writeInt32 1
do! writeInt32 2
do! writeInt32 3
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With