简体   繁体   English

F#异步工作流/任务与免费monad相结合

[英]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: 我正在尝试使用免费的monad模式构建用于消息处理的管道,我的代码看起来像这样:

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? 然而,让这些方法异步是很重要的( Task最好,但Async是可以接受的),但是当我为我的pipeline创建一个构建器时,我无法弄清楚如何使用它 - 我怎么能等待一个Task<'msgOut>Async<'msgOut>所以我可以把它发送出来并等待这个“发送”任务?

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>>> 返回PipeProgram<'b, 'a, Async<PipeProgram<'c, 'a, Async>>>

First of all, I think that using free monads in F# is very close to being an anti-pattern. 首先,我认为在F#中使用免费monad非常接近于反模式。 It is a very abstract construction that does not fit all that great with idiomatic F# style - but that is a matter of preference and if you (and your team) finds this way of writing code readable and easy to understand, then you can certainly go in this direction. 这是一个非常抽象的结构,不适合用惯用的F#风格 - 但这是一个偏好的问题,如果你(和你的团队)发现这种编写代码可读且易于理解的方式,那么你当然可以去在这个方向。

Out of curiosity, I spent a bit of time playing with your example - although I have not quite figured out how to fix your example completely, I hope the following might help to steer you in the right direction. 出于好奇,我花了一些时间玩你的例子 - 虽然我还没有弄清楚如何完全修复你的例子,但我希望以下可能有助于引导你朝着正确的方向前进。 The summary is that I think you will need to integrate Async into your PipeProgram so that the pipe program is inherently asynchronous: 总结是,我认为您需要将Async集成到PipeProgram以便管道程序本质上是异步的:

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

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

Note that I had to add Continue to make my functions type-check, but I think that's probably a wrong hack and you might need to remote that. 请注意,我必须添加Continue以使我的函数类型检查,但我认为这可能是一个错误的黑客,你可能需要远程。 With these definitions, you can then do: 通过这些定义,您可以执行以下操作:

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

let rec bind (f:'a -> PipeProgram<_, _, _>) = function
    | Act x -> 
        let w = async { 
          let! x = x 
          return mapInstruction (bind f) x }
        Act w
    | Stop x -> 
        let w = async {
          let! x = x
          let pg = f x
          return Continue pg
        }
        Act w

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

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

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

pipeline ignore 0 

This now gives you just plain PipeProgram<int, unit, unit> which you should be able to evaluate by having a recursive asynchronous functions that acts on the commands. 现在,这将为您提供简单的PipeProgram<int, unit, unit> ,您应该能够通过具有作用于命令的递归异步函数来评估它。

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. 在我的理解中,自由monad的重点在于你没有暴露像Async这样的效果,所以我不认为它们应该在PipeInstruction类型中使用。 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. 此外,Free Monad真的只在Haskell中有意义,你需要做的就是定义一个仿函数,然后你自动完成其余的实现。 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. 在F#中,您还必须编写其余的代码,因此使用Free比传统的解释器模式没有多大好处。 That TurtleProgram code you linked to was just an experiment -- I would not recommend using Free for real code at all. 您链接到的TurtleProgram代码只是一个实验 - 我不建议使用Free代替实际代码。

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: 无论如何,如果你确实想要编写一个解释器版本(而不是Free),我就是这样做的:

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): 这是一个非异步版本的解释器(“Id monad”,就像它一样):

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM