Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How is this F# workflow evaluated?

Tags:

f#

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?

like image 494
pavuxun Avatar asked Sep 03 '25 02:09

pavuxun


1 Answers

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
}
like image 78
Tomas Petricek Avatar answered Sep 04 '25 23:09

Tomas Petricek