【问题标题】:F# passing state into a function in BindF# 将状态传递给 Bind 中的函数
【发布时间】:2021-03-18 17:27:30
【问题描述】:

这是我在此处提出的问题 (F# Binding to output while carrying the state) 的演变。

我正在尝试让Bind 方法获取具有此签名PlanAccumulator<'a> -> PlanAccumulator<'b> 的函数。 getFood 函数就是一个例子。我希望Bind 方法使用PlanAccumulator<'a> 对象调用getFood

挑战在于让计算表达式 (CE) 使用 CE 中该点存在的 PlanAccumulator 调用 Bind 方法内的 getFood 函数。我不知道该怎么做,但我觉得应该可以。

type StepId = StepId of int
type State = {
  LastStepId : StepId
}

type Food =
    | Chicken
    | Rice

type Step =
  | GetFood of StepId * Food
  | Eat of StepId * Food
  | Sleep of StepId * duration:int

type PlanAccumulator<'T> = PlanAccumulator of State * Step list * 'T

let rng = System.Random(123)

let getFood (PlanAccumulator (s, p, r)) =
  printfn "GetFood"
  let randomFood = 
    if rng.NextDouble() > 0.5 then Food.Chicken
    else Food.Rice
  let (StepId lastStepId) = s.LastStepId
  let nextStepId = StepId (lastStepId + 1)
  let newState = { s with LastStepId = nextStepId }
  let newStep = GetFood (nextStepId, randomFood)
  PlanAccumulator (newState, newStep::p, randomFood)

type PlanBuilder (state: State) =

    member this.For (PlanAccumulator (state, steps1, res):PlanAccumulator<'T>, f:'T -> PlanAccumulator<'R>) : PlanAccumulator<'R> =
      printfn "For"
      let (PlanAccumulator(state2, steps2, res2)) = f res
      PlanAccumulator (state2, steps2 @ steps1, res2)

    member this.Bind (input:PlanAccumulator<'a> -> PlanAccumulator<'T>, f:'T -> PlanAccumulator<'R>) : PlanAccumulator<'R> =
        printfn "Bind"
        // THIS IS THE PROBLEM: How do I get the previous PlanAccumulator to 
        // this point in the computation?
        let PlanAccumulator (state1, steps1, res) = input previousAccumulator 
        let (PlanAccumulator(state2, steps2, res2)) = f (state1 res)
        PlanAccumulator (state2, steps2 @ steps1, res2)

    member this.Yield x = 
        printfn "Yield"
        PlanAccumulator (state, [], x)

    member this.Run (PlanAccumulator (s, p, r)) = 
        printfn "Run"
        s, List.rev p

    [<CustomOperation("eat", MaintainsVariableSpace=true)>]
    member this.Eat (PlanAccumulator(s, p, r), [<ProjectionParameter>] food) =
        printfn $"Eat: {food}"
        let (StepId lastStepId) = s.LastStepId
        let nextStepId = StepId (lastStepId + 1)
        let newState = { s with LastStepId = nextStepId }
        let newStep = Eat (nextStepId, (food r))
        PlanAccumulator (newState, newStep::p, r)

    [<CustomOperation("sleep", MaintainsVariableSpace=true)>]
    member this.Sleep (PlanAccumulator (s, p, r), [<ProjectionParameter>] duration) =
        printfn $"Sleep: {duration}"
        let (StepId lastStepId) = s.LastStepId
        let nextStepId = StepId (lastStepId + 1)
        let newState = { s with LastStepId = nextStepId }
        let newStep = Sleep (nextStepId, (duration r))
        PlanAccumulator (newState, newStep::p, r)

// let plan = PlanBuilder()
let initialState = {
  LastStepId = StepId 0
}

let newState, testPlan =
  PlanBuilder initialState {
      let! food = getFood
      sleep 5
      eat Chicken
  }

以下是testPlan 的示例,如果它按预期工作:

val testPlan : Step list =
    [
        (StepId 1, GetFood Chicken)
        (StepId 2, Sleep 1)
        (StepId 3, Eat Chicken)
    ]

【问题讨论】:

    标签: f# monads computation-expression


    【解决方案1】:

    我认为你想要一个普通的旧状态 monad,你可以看到 here。使用这个,我破解了你的代码如下:

    type State<'s, 'a> = State of ('s -> ('a * 's))
    
    module State =
        let inline run state x = let (State(f)) = x in f state
        let get = State(fun s -> s, s)
        let put newState = State(fun _ -> (), newState)
        let map f s = State(fun (state: 's) ->
            let x, state = run state s
            f x, state)
    
    /// The state monad passes around an explicit internal state that can be
    /// updated along the way. It enables the appearance of mutability in a purely
    /// functional context by hiding away the state when used with its proper operators
    /// (in StateBuilder()). In other words, you implicitly pass around an implicit
    /// state that gets transformed along its journey through pipelined code.
    type StateBuilder() =
        member this.Zero () = State(fun s -> (), s)
        member this.Return x = State(fun s -> x, s)
        member inline this.ReturnFrom (x: State<'s, 'a>) = x
        member this.Bind (x, f) : State<'s, 'b> =
            State(fun state ->
                let (result: 'a), state = State.run state x
                State.run state (f result))
        member this.Combine (x1: State<'s, 'a>, x2: State<'s, 'b>) =
            State(fun state ->
                let result, state = State.run state x1
                State.run state x2)
        member this.Delay f : State<'s, 'a> = f ()
        member this.For (seq, (f: 'a -> State<'s, 'b>)) =
            seq
            |> Seq.map f
            |> Seq.reduceBack (fun x1 x2 -> this.Combine (x1, x2))
        member this.While (f, x) =
            if f () then this.Combine (x, this.While (f, x))
            else this.Zero ()
    
    let state = new StateBuilder()
    
    type StepId = StepId of int
    
    type PlanState = {
        LastStepId : StepId
    }
    
    type Food =
        | Chicken
        | Rice
    
    type Step =
        | GetFood of StepId * Food
        | Eat of StepId * Food
        | Sleep of StepId * duration:int
    
    type PlanAccumulator = PlanAccumulator of PlanState * Step list
    
    let rng = System.Random(123)
    
    let getFood =
        state {
            printfn "GetFood"
            let randomFood = 
                if rng.NextDouble() > 0.5 then Food.Chicken
                else Food.Rice
            let! (PlanAccumulator (planState, steps)) = State.get
            let (StepId lastStepId) = planState.LastStepId
            let nextStepId = StepId (lastStepId + 1)
            let newState = { planState with LastStepId = nextStepId }
            let newStep = GetFood (nextStepId, randomFood)
            do! State.put (PlanAccumulator (newState, newStep :: steps))
            return randomFood
        }
    
    let eat food =
        state {
            printfn "Eat: %A" food
            let! (PlanAccumulator (planState, steps)) = State.get
            let (StepId lastStepId) = planState.LastStepId
            let nextStepId = StepId (lastStepId + 1)
            let newState = { planState with LastStepId = nextStepId }
            let newStep = Eat (nextStepId, food)
            do! State.put (PlanAccumulator (newState, newStep :: steps))
        }
    
    let sleep duration =
        state {
            printfn "Sleep: %A" duration
            let! (PlanAccumulator (planState, steps)) = State.get
            let (StepId lastStepId) = planState.LastStepId
            let nextStepId = StepId (lastStepId + 1)
            let newState = { planState with LastStepId = nextStepId }
            let newStep = Sleep (nextStepId, duration)
            do! State.put (PlanAccumulator (newState, newStep :: steps))
        }
    
    let initialState = {
        LastStepId = StepId 0
    }
    
    let initialPlan =
        PlanAccumulator (initialState, List.empty)
    
    [<EntryPoint>]
    let main argv =
    
        let _, testPlan =
            state {
                let! food = getFood
                do! sleep 10
                do! eat food
            } |> State.run initialPlan
    
        printfn "%A" testPlan
    
        0
    

    输出是:

    GetFood
    Sleep: 10
    Eat: Chicken
    PlanAccumulator
      ({ LastStepId = StepId 3 },
       [Eat (StepId 3, Chicken); Sleep (StepId 2, 10); GetFood (StepId 1, Chicken)])
    

    【讨论】:

    • 您知道是否仍然可以使用自定义操作吗?
    • 我不确定。我认为你可以从StateBuilder 继承你的PlanBuilder 并在那里实现它们。我会看看我是否可以相应地更新我的答案。
    • 更新:我无法弄清楚如何使用自定义操作来做到这一点。问题是sleep 10(没有do!)试图产生一个值,这不是你想要的。不过,我以前从未做过自定义操作,所以比我更有经验的人可能会弄清楚。 (也就是说,您仍然可以使用 let! 使用 getFood,这似乎不一致,所以我有点怀疑在这种情况下自定义操作的适当性。)
    • 这给了我足够的工作时间。我可以从这里提炼。感谢您的洞察力!
    猜你喜欢
    • 2019-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多