Search code examples
asynchronousf#async-awaitfree-monadcomputation-expression

F# async workflow / tasks combined with free monad


I'm trying to build pipeline for message handling using free monad pattern, my code looks like that:

module PipeMonad =
type PipeInstruction<'msgIn, 'msgOut, 'a> =
    | HandleAsync of 'msgIn * (Async<'msgOut> -> 'a)
    | SendOutAsync of 'msgOut * (Async -> 'a)

let private mapInstruction f = function
    | HandleAsync (x, next) -> HandleAsync (x, next >> f)
    | SendOutAsync (x, next) -> SendOutAsync (x, next >> f)

type PipeProgram<'msgIn, 'msgOut, 'a> =
    | Act of PipeInstruction<'msgIn, 'msgOut, PipeProgram<'msgIn, 'msgOut, 'a>>
    | Stop of 'a

let rec bind f = function
    | Act x -> x |> mapInstruction (bind f) |> Act
    | Stop x -> f x

type PipeBuilder() =
    member __.Bind (x, f) = bind f x
    member __.Return x = Stop x
    member __.Zero () = Stop ()
    member __.ReturnFrom x = x

let pipe = PipeBuilder()
let handleAsync msgIn = Act (HandleAsync (msgIn, Stop))
let sendOutAsync msgOut = Act (SendOutAsync (msgOut, Stop))

which I wrote according to this article

However it's important to me to have those methods asynchronous (Task preferably, but Async is acceptable), but when I created a builder for my pipeline, I can't figure out how to use it - how can I await a Task<'msgOut> or Async<'msgOut> so I can send it out and await this "send" task?

Now I have this piece of code:

let pipeline log msgIn =
    pipe {
        let! msgOut = handleAsync msgIn
        let result = async {
            let! msgOut = msgOut
            log msgOut
            return sendOutAsync msgOut
        }
        return result
    }

which returns PipeProgram<'b, 'a, Async<PipeProgram<'c, 'a, Async>>>


Solution

  • In my understanding, the whole point of the free monad is that you don't expose effects like Async, so I don't think they should be used in the PipeInstruction type. The interpreter is where the effects get added.

    Also, the Free Monad really only makes sense in Haskell, where all you need to do is define a functor, and then you get the rest of the implementation automatically. In F# you have to write the rest of the code as well, so there is not much benefit to using Free over a more traditional interpreter pattern. That TurtleProgram code you linked to was just an experiment -- I would not recommend using Free for real code at all.

    Finally, if you already know the effects you are going to use, and you are not going to have more than one interpretation, then using this approach doesn't make sense. It only makes sense when the benefits outweigh the complexity.

    Anyway, if you did want to write an interpreter version (rather than Free) this is how I would do it:

    First, define the instructions without any effects.

    /// The abstract instruction set
    module PipeProgram =
    
        type PipeInstruction<'msgIn, 'msgOut,'state> =
            | Handle of 'msgIn * ('msgOut -> PipeInstruction<'msgIn, 'msgOut,'state>)
            | SendOut of 'msgOut * (unit -> PipeInstruction<'msgIn, 'msgOut,'state>)
            | Stop of 'state
    

    Then you can write a computation expression for it:

    /// A computation expression for a PipeProgram
    module PipeProgramCE =
        open PipeProgram
    
        let rec bind f instruction =
            match instruction with
            | Handle (x,next) ->  Handle (x, (next >> bind f))
            | SendOut (x, next) -> SendOut (x, (next >> bind f))
            | Stop x -> f x
    
        type PipeBuilder() =
            member __.Bind (x, f) = bind f x
            member __.Return x = Stop x
            member __.Zero () = Stop ()
            member __.ReturnFrom x = x
    
    let pipe = PipeProgramCE.PipeBuilder()
    

    And then you can start writing your computation expressions. This will help flush out the design before you start on the interpreter.

    // helper functions for CE
    let stop x = PipeProgram.Stop x
    let handle x = PipeProgram.Handle (x,stop)
    let sendOut x  = PipeProgram.SendOut (x, stop)
    
    let exampleProgram : PipeProgram.PipeInstruction<string,string,string> = pipe {
        let! msgOut1 = handle "In1"
        do! sendOut msgOut1
        let! msgOut2 = handle "In2"
        do! sendOut msgOut2
        return msgOut2
        }
    

    Once you have described the the instructions, you can then write the interpreters. And as I said, if you are not writing multiple interpreters, then perhaps you don't need to do this at all.

    Here's an interpreter for a non-async version (the "Id monad", as it were):

    module PipeInterpreterSync =
        open PipeProgram
    
        let handle msgIn =
            printfn "In: %A"  msgIn
            let msgOut = System.Console.ReadLine()
            msgOut
    
        let sendOut msgOut =
            printfn "Out: %A"  msgOut
            ()
    
        let rec interpret instruction =
            match instruction with
            | Handle (x, next) ->
                let result = handle x
                result |> next |> interpret
            | SendOut (x, next) ->
                let result = sendOut x
                result |> next |> interpret
            | Stop x ->
                x
    

    and here's the async version:

    module PipeInterpreterAsync =
        open PipeProgram
    
        /// Implementation of "handle" uses async/IO
        let handleAsync msgIn = async {
            printfn "In: %A"  msgIn
            let msgOut = System.Console.ReadLine()
            return msgOut
            }
    
        /// Implementation of "sendOut" uses async/IO
        let sendOutAsync msgOut = async {
            printfn "Out: %A"  msgOut
            return ()
            }
    
        let rec interpret instruction =
            match instruction with
            | Handle (x, next) -> async {
                let! result = handleAsync x
                return! result |> next |> interpret
                }
            | SendOut (x, next) -> async {
                do! sendOutAsync x
                return! () |> next |> interpret
                }
            | Stop x -> x