F# 中的随机/状态工作流

Random / State workflow in F#

我正在努力思考 F# 中的 mon-, err, 工作流,虽然我认为我对基本 "Maybe" 工作流有相当扎实的理解,但我正在尝试实现状态工作流以生成随机数真的让我难住了。

我未完成的尝试可以在这里看到:

let randomInt state =
    let random = System.Random(state)
    // Generate random number and a new state as well
    random.Next(0,1000), random.Next() 

type RandomWF (initState) =
    member this.Bind(rnd,rest) =
        let value, newState = rnd initState
        // How to feed "newState" into "rest"??
        value |> rest
    member this.Return a = a // Should I maybe feed "initState" into the computation here?

RandomWF(0) {
    let! a = randomInt
    let! b = randomInt
    let! c = randomInt
    return [a; b; c]
} |> printfn "%A"

编辑:真的让它起作用了!虽然不确定它是如何工作的,所以如果有人想把它放在一个好的答案中,它仍然有待解决。这是我的工作代码:

type RandomWF (initState) =
    member this.Bind(rnd,rest) =
        fun state ->
            let value, nextState = rnd state
            rest value nextState

    member this.Return a = fun _ -> a 

    member this.Run x = x initState

有两件事让您更难看到您的工作流程在做什么:

  1. 您正在为您的 monad 类型使用函数类型,
  2. 您的工作流不仅建立了计算,而且运行它也是。

我认为,一旦您看到没有这两个障碍的情况,就会更清楚地遵循它。下面是使用 DU 包装器类型定义的工作流程:

type Random<'a> = 
    Comp of (int -> 'a * int)

let run init (Comp f) = f init

type Random<'a> with 
    member this.Run(state) = fst <| run state this 

type RandomBuilder() =
    member this.Bind(Comp m, f: 'a -> Random<_>) =
        Comp <| fun state ->
            let value, nextState = m state
            let comp = f value
            run nextState comp             

    member this.Return(a) = Comp (fun s -> a, s)

let random = RandomBuilder()

下面是你如何使用它:

let randomInt =
    Comp <| fun state ->
        let rnd = System.Random(state)
        rnd.Next(0,1000), rnd.Next()

let rand =
    random {
        let! a = randomInt
        let! b = randomInt
        let! c = randomInt
        return [a; b; c ]
    }

rand.Run(0)
|> printfn "%A"

在此版本中,您单独构建计算(并将其存储在 Random 类型中),然后 运行 将其传入初始状态。查看构建器方法的类型是如何推断出来的,并将它们与 MSDN documentation 描述的内容进行比较。

编辑:构造一次构建器对象并将绑定用作各种别名主要是约定俗成,但它有充分的理由,因为构建器是无状态的是有意义的.我明白为什么参数化构建器看起来是一个有用的特性,但老实说,我无法想象一个令人信服的用例。

monad 的主要卖点是计算的定义执行 的分离。

在你的情况下——你想要做的是对你的计算进行表示,并能够运行它具有某种状态——也许是 0,也许是 42。你不需要知道初始状态以定义将使用它的计算。通过将状态传递给构建器,您最终模糊了定义和执行之间的界限,这只会降低工作流的用处。

将其与 async 工作流程进行比较 - 当您编写异步块时,您不会使代码 运行 异步。您只创建一个 Async<'a> 对象来表示一个计算,当您 运行 它时,该计算将产生一个 'a 的对象 - 但是您如何做,取决于您。建造者不需要知道。