F# 说值未在计算表达式中定义

Mat*_*ews 8 monads f# computation-expression

我一直在研究带有 F# 计算表达式的 State Monad,我也在尝试利用自定义操作。我得到了一些没有意义的奇怪行为。编译器报告一个值在上面两行声明时不存在。

type State<'a, 's> = ('s -> 'a * 's)

module State =
    // Explicit
    // let result x : State<'a, 's> = fun s -> x, s
    // Less explicit but works better with other, existing functions:
    let result x s = 
        x, s

    let bind (f:'a -> State<'b, 's>) (m:State<'a, 's>) : State<'b, 's> =
        // return a function that takes the state
        fun s ->
            // Get the value and next state from the m parameter
            let a, s' = m s
            // Get the next state computation by passing a to the f parameter
            let m' = f a
            // Apply the next state to the next computation
            m' s'

    /// Evaluates the computation, returning the result value.
    let eval (m:State<'a, 's>) (s:'s) = 
        m s 
        |> fst

    /// Executes the computation, returning the final state.
    let exec (m:State<'a, 's>) (s:'s) = 
        m s
        |> snd

    /// Returns the state as the value.
    let getState (s:'s) = 
        s, s

    /// Ignores the state passed in favor of the provided state value.
    let setState (s:'s) = 
        fun _ -> 
            (), s


type StateBuilder() =
    member __.Return(value) : State<'a, 's> = 
        State.result value
    member __.Bind(m:State<'a, 's>, f:'a -> State<'b, 's>) : State<'b, 's> = 
        State.bind f m
    member __.ReturnFrom(m:State<'a, 's>) = 
        m
    member __.Zero() =
        State.result ()
    member __.Delay(f) = 
        State.bind f (State.result ())


let rng = System.Random(123)
type StepId = StepId of int
type Food =
    | Chicken
    | Rice
type Step =
  | GetFood of StepId * Food
  | Eat of StepId * Food
  | Sleep of StepId * duration:int
type PlanAcc = PlanAcc of lastStepId:StepId * steps:Step list

let state = StateBuilder()

let getFood =
    state {
        printfn "GetFood"
        let randomFood = 
            if rng.NextDouble() > 0.5 then Food.Chicken
            else Food.Rice
        let! (PlanAcc (StepId lastStepId, steps)) = State.getState
        let nextStepId = StepId (lastStepId + 1)
        let newStep = GetFood (nextStepId, randomFood)
        let newAcc = PlanAcc (nextStepId, newStep::steps)
        do! State.setState newAcc
        return randomFood
    }

let sleepProgram duration = 
    state {
        printfn "Sleep: %A" duration
        let! (PlanAcc (StepId lastStepId, steps)) = State.getState
        let nextStepId = StepId (lastStepId + 1)
        let newStep = Sleep (nextStepId, duration)
        let newAcc = PlanAcc (nextStepId, newStep::steps)
        do! State.setState newAcc
    }

let eatProgram food =
    state {
        printfn "Eat: %A" food
        let! (PlanAcc (StepId lastStepId, steps)) = State.getState
        let nextStepId = StepId (lastStepId + 1)
        let newStep = Eat (nextStepId, food)
        let newAcc = PlanAcc (nextStepId, newStep::steps)
        do! State.setState newAcc
    }

type StateBuilder with

    [<CustomOperation("sleep", MaintainsVariableSpaceUsingBind=true)>]
    member this.Sleep (state:State<_,PlanAcc>, duration) =
        printfn $"Sleep"
        State.bind (fun _ -> sleepProgram duration) state

    [<CustomOperation("eat", MaintainsVariableSpaceUsingBind=true)>]
    member this.Eat (state:State<_,PlanAcc>, food) =
        printfn $"Eat"
        State.bind (fun _ -> eatProgram food) state


let simplePlan =
    state {
        let! food = getFood
        sleep 2
        eat food // <-- This is where the error is. 
                 // The value or constructor 'food' does not exist
    }

let initalAcc = PlanAcc(StepId 0, [])

let x = State.exec simplePlan initalAcc
x
Run Code Online (Sandbox Code Playgroud)

这是错误的图片: 在此处输入图片说明

Fyo*_*kin 20

这一切都与计算表达式的深层性质有关,根据您在帖子中放置的标签判断,您必须已经了解monads

什么是单子?它只是这种将计算链接在一起的模式的名称,将一个计算的结果作为参数传递给下一个,仅此而已。有关示例的更全面的解释,请参阅此答案。下面我就假设你知道如何bindreturn工作,尤其是看到你是如何实现它们State自己。

什么是计算表达式?它们是您通常所说的“monad 理解”,这基本上意味着它们是monad 的语法糖。实际上,这意味着它们是聪明的语法,最终会被分解为一系列的bindandreturn调用。

让我们考虑一个没有 的简化示例sleep

state {
  let! food = getFood
  printfn $"{food}"
}
Run Code Online (Sandbox Code Playgroud)

此代码将脱糖为:

state.Bind(
  getFood,
  (fun food ->
    printfn "${food}"
    state.Return ()
  )
)
Run Code Online (Sandbox Code Playgroud)

看看这里发生了什么?之后的计算部分getFood变成了一个函数,这个函数food作为一个参数。这就是该printfn行获取foodto print值的方式 - 凭借它作为参数传递给函数。

但是,自定义操作的工作方式略有不同。当编译器遇到自定义操作时,它会获取自定义操作之前的整个表达式(Bind调用序列),并将整个内容作为参数传递给自定义操作。

为了看看会发生什么,让我们尝试eat

state {
  let! food = getFood
  printfn $"{food}"
  eat food
}
Run Code Online (Sandbox Code Playgroud)

这将被脱糖为:

state.Eat(
  state.Bind(
    getFood,
    (fun food ->
      printfn $"{food}"
      state.Return food
    )
  ),
  food
)
Run Code Online (Sandbox Code Playgroud)

嗯……看看这里发生了什么?的第二个参数Eatis food,但没有在任何地方定义!它只在嵌套函数内有效!这是您遇到错误的地方。

所以为了解决这个问题,计算表达式有一个特殊的东西:ProjectionParameterAttribute. 这里的“投影”一词粗略地表示“转换”,其想法是这样的参数将是一个函数,可以在“到目前为止”计算的计算结果上调用它以提取它的某些部分。

在实践中,这意味着如果我们这样注释Eat

member this.Eat (state:State<_,PlanAcc>, [<ProjectionParameter>] food) =
Run Code Online (Sandbox Code Playgroud)

那么上面例子的脱糖就变成这样了:

state.Eat(
  state.Bind(
    getFood,
    (fun food ->
      printfn $"{food}"
      state.Return(food)
    )
  ),
  (fun x -> x)
)
Run Code Online (Sandbox Code Playgroud)

注意嵌套函数是如何调用的state.Return,因此整个Eat第一个参数的结果是 的值food。这是故意完成的,以使中间变量可用于计算的下一部分。这就是“维护可变空间”的意思。

然后注意第二个参数是如何Eat变成的fun x -> x——这意味着它food从中间状态中提取 的值,该中间状态已经Eat通过 that从第一个参数返回state.Return

现在Eat实际上可以调用该函数来获取 的值food

member this.Eat (state:State<_,PlanAcc>, [<ProjectionParameter>] food) =
    printfn $"Eat"
    State.bind (fun x -> eatProgram (food x)) state
Run Code Online (Sandbox Code Playgroud)

请注意参数x- 来自state,通过 汇集到 lambda 表达式中State.bind。如果您查看 的类型Eat,您会发现它变成了这样:

Eat : State<'a, StateAcc> * ('a -> Food) -> State<unit, StateAcc>
Run Code Online (Sandbox Code Playgroud)

这意味着它需要一个产生 some 的状态计算'a,加上一个来自'ato的函数Food,它返回一个不产生任何东西的状态计算(即unit)。

到现在为止还挺好。这将解决“food未定义”问题。


但没那么快!现在你有一个新的问题。尝试引入sleep

state {
  let! food = getFood
  printfn $"{food}"
  sleep 2
  eat food
}
Run Code Online (Sandbox Code Playgroud)

现在你得到一个新的错误:food本来应该有 type Food,但这里有 type unit

WTF在这里进行?!

好吧,你只是扔掉了food内部Sleep,仅此而已。

    member this.Sleep (state:State<_,PlanAcc>, duration) =
        printfn $"Sleep"
        State.bind (fun _ -> sleepProgram duration) state
                        ^
                        |
                    This was `food`. It's gone now.
Run Code Online (Sandbox Code Playgroud)

你看,Sleep进行一个计算产生的东西,然后扔掉那个东西然后运行sleepProgram,这是一个计算产生的unit,所以这就是结果sleep

让我们看看脱糖后的代码:

state.Eat(
  state.Sleep(
    state.Bind(
      getFood,
      (fun food ->
        printfn $"{food}"
        state.Return food
      )
    ),
    2
  )
  (fun x -> x)
)
Run Code Online (Sandbox Code Playgroud)

看看Sleep第一个参数的结果如何Eat?这意味着Sleep需要返回一个计算生成food,以便Eat的第二个参数可以访问它。但Sleep没有。它返回 的结果sleepProgram,这是一个产生 的计算unit。所以food现在没了。

什么Sleep真正需要做的是第一次运行sleepProgram,然后到它的结束链中的另一个计算,将返回原来的结果Sleep的第一个参数。像这样:

member this.Sleep (state:State<_,PlanAcc>, duration) =
  printfn $"Sleep"
  State.bind 
    (fun x -> 
      State.bind 
        (fun () -> State.result x) 
        (sleepProgram duration)
    ) 
    state
Run Code Online (Sandbox Code Playgroud)

但这太丑了,不是吗?幸运的是,我们有一个方便的编译器功能可以将这些混乱的bind调用变成一个漂亮而干净的程序:计算表达式!

member this.Sleep (st:State<_,PlanAcc>, duration) =
  printfn $"Sleep"
  state {
    let! x = st
    do! sleepProgram duration
    return x 
  }
Run Code Online (Sandbox Code Playgroud)

如果你从这一切中拿走一件事,让它成为以下内容:

在计算表达式中定义的“变量”根本不是真正的“变量”,它们只是看起来像它们,但实际上它们是函数参数,您必须这样对待它们。这意味着每个操作都必须确保遍历从上游获得的任何参数。否则,这些“变量”将无法在下游使用。

  • 很高兴我能帮上忙。我有时确实会对博客文章采用这样的答案(查看[我们的博客](https://medium.com/collegevine-product)),但这并不像复制和粘贴那么简单。一篇博客文章“突然”出现在您面前,因此它需要在提供解决方案之前解释问题。在这里,你已经为我解决了问题部分。 (3认同)
  • 这个答案真是太棒了。谢谢你!这本身应该是一篇博文。 (2认同)
  • 您会很高兴知道我能够成功更新 `Sleep` 方法以也使用 `[&lt;ProjectionParameter&gt;]` 并使用先前在 CE 中声明的值。 (2认同)