简体   繁体   English

F# 表示计算表达式中未定义的值

[英]F# saying value not defined in Computation Expression

I've been working on a State Monad with F# Computation Expression and I'm trying to also utilize Custom Operations.我一直在研究带有 F# 计算表达式的 State Monad,并且我也在尝试使用自定义操作。 I'm getting some weird behavior that does not make sense.我得到了一些没有意义的奇怪行为。 The compiler is reporting that a value does not exist when it was declared just two lines above.编译器报告一个值在上面仅声明两行时不存在。

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

Here's a picture of the error:这是错误的图片: 在此处输入图像描述

This all has to do with the deep nature of computation expressions, which, judging by the tags you put on your post, you must already understand are monads .这一切都与计算表达式的深层性质有关,从您在帖子中放置的标签来看,您必须已经了解monads

What are monads?什么是单子? It's just a name for this pattern of chaining computations together, passing the result of one as parameter to the next, that's all.它只是这种将计算链接在一起的模式的名称,将一个结果作为参数传递给下一个,仅此而已。 See this answer for a somewhat more comprehensive explanation with examples.有关示例的更全面解释,请参见此答案 Here I'll just assume you know how bind and return work, especially seeing how you've implemented them for State yourself.在这里,我假设您知道bindreturn是如何工作的,尤其是看看您自己是如何为State实现它们的。

And what are computation expressions?什么是计算表达式? They're what you might more generally call "monad comprehensions", which basically means they're syntactic sugar for monads.它们就是你通常所说的“单子理解”,这基本上意味着它们是单子的语法糖 In practical terms, this means that they're clever syntax, which ultimately gets desugared to a series of bind and return calls.实际上,这意味着它们是聪明的语法,最终会变成一系列bindreturn调用。

Let's consider a simplified example without sleep :让我们考虑一个没有sleep的简化示例:

state {
  let! food = getFood
  printfn $"{food}"
}

This code would desugar into this:这段代码会变成这样:

state.Bind(
  getFood,
  (fun food ->
    printfn "${food}"
    state.Return ()
  )
)

See what happened here?看看这里发生了什么? The part of the computation that comes after getFood got turned into a function, and this function takes food as a parameter. getFood之后的计算部分变成了 function,而这个 function 以food为参数。 That's how the printfn line gets the value of food to print - by virtue of it being passed as a parameter to the function.这就是printfn行获取要打印的food值的方式——因为它作为参数传递给 function。

Custom operations, however, work a bit differently.然而,自定义操作的工作方式略有不同。 When the compiler encounters a custom operation, it takes the whole expression (the sequence of Bind calls) that came before the custom operation, and passes that whole thing to the custom operation as a parameter.当编译器遇到自定义操作时,它会获取自定义操作之前的整个表达式( Bind调用的序列),并将整个内容作为参数传递给自定义操作。

To see what happens, let's try to eat :看看会发生什么,让我们尝试eat

state {
  let! food = getFood
  printfn $"{food}"
  eat food
}

This would get desugared to:这将被取消:

state.Eat(
  state.Bind(
    getFood,
    (fun food ->
      printfn $"{food}"
      state.Return food
    )
  ),
  food
)

Hmm... See what happened here?嗯……看看这里发生了什么? The second parameter of Eat is food , but that's not defined anywhere. Eat的第二个参数是food ,但在任何地方都没有定义。 It's only valid inside that nested function!它仅在该嵌套函数内有效! This is where you're getting your error.这是你得到错误的地方。

So to deal with this, computation expressions have a special thing: ProjectionParameterAttribute .所以为了解决这个问题,计算表达式有一个特殊的东西: ProjectionParameterAttribute Here the word "projection" roughly means "transformation", and the idea is that such parameter would be a function , which can be called on the result of the computation that's been computed "so far" to extract some part of it.这里的“投影”一词大致意思是“转换”,其想法是这样的参数将是function ,可以在“到目前为止”计算的计算结果上调用它以提取其中的一部分。

In practice this means that if we annotate Eat like so:在实践中,这意味着如果我们这样注释Eat

member this.Eat (state:State<_,PlanAcc>, [<ProjectionParameter>] food) =

Then the desugaring of the above example becomes this:那么上面例子的脱糖就变成了这样:

state.Eat(
  state.Bind(
    getFood,
    (fun food ->
      printfn $"{food}"
      state.Return(food)
    )
  ),
  (fun x -> x)
)

Notice how the nested function calls state.Return , so that the result of the whole Eat 's first parameter is the value of food .注意嵌套的 function 是如何调用state.Return的,所以整个Eat的第一个参数的结果就是food的值。 This is done on purpose, to make intermediate variables available to the next part of the computation.这是故意完成的,以使中间变量可用于下一部分计算。 This is what it means to "maintain variable space".这就是“保持可变空间”的意思。

And then notice how the second parameter of Eat became fun x -> x - meaning it's extracting the value of food from the intermediate state that has been returned from the Eat 's first parameter via that state.Return .然后注意Eat的第二个参数如何变得fun x -> x - 意味着它从中间 state 中提取food的值,该中间值是通过该state.ReturnEat的第一个参数返回的。

Now Eat can actually call that function to get at the value of food .现在Eat实际上可以调用 function 来获取food的值。

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

Note the parameter x - that comes from state , funneled into the lambda expression by State.bind .注意参数x - 来自state ,通过State.bind到 lambda 表达式中。 If you look at the type of Eat , you'll see that it became this:如果您查看Eat的类型,您会发现它变成了这样:

Eat : State<'a, StateAcc> * ('a -> Food) -> State<unit, StateAcc>

Meaning that it takes a state computation producing some 'a , plus a function from 'a to Food , and it returns a state computation producing nothing (ie unit ).这意味着它需要一个 state 计算产生一些'a ,加上一个从'aFood的 function ,它返回一个 state 计算什么都不产生(即unit )。

So far so good.到目前为止,一切都很好。 This will fix the " food is not defined " problem.这将解决“ food未定义”的问题。


But not so fast.但没那么快。 Now you have a new problem.现在你有一个新问题。 Try introducing sleep back in:尝试重新引入sleep

state {
  let! food = getFood
  printfn $"{food}"
  sleep 2
  eat food
}

And now you get a new error: food was expected to have type Food , but here has type unit .现在你得到一个新的错误: food应该有Food类型,但是这里有unit类型。

WTF is going on here?! WTF在这里发生?!

Well, you're just throwing away the food inside Sleep , that's all.好吧,你只是把Sleep里面的food扔掉了,仅此而已。

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

You see, Sleep takes a computation producing something and proceeds to throw away that something and run sleepProgram , which is a computation producing unit , so that's what the result of sleep becomes.你看, Sleep需要一个计算产生一些东西,然后扔掉那个东西并运行sleepProgram ,它是一个计算产生unit ,所以这就是sleep的结果。

Let's look at the desugared code:让我们看一下脱糖的代码:

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

See how Sleep 's result is the first parameter of Eat ?看看Sleep的结果如何是Eat的第一个参数? That means Sleep needs to return a computation producing food , so that Eat 's second parameter can have access to it.这意味着Sleep需要返回一个产生food的计算,以便Eat的第二个参数可以访问它。 But Sleep doesn't.Sleep没有。 It returns the result of sleepProgram , which is a computation producing unit .它返回sleepProgram的结果,它是一个计算生成unit So food is gone now.所以现在food没有了。

What Sleep really needs to do is to first run sleepProgram , then to the end of it chain another computation that would return the result of the original Sleep 's first parameter. Sleep真正需要做的是首先运行sleepProgram ,然后在它的最后链接另一个计算,该计算将返回原始Sleep的第一个参数的结果。 Like this:像这样:

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

But this is ugly as hell, isn't it?但这太丑了,不是吗? Luckily, we have a handy compiler feature to turn this mess of bind calls into a nice and clean program: computation expressions!幸运的是,我们有一个方便的编译器功能,可以将这种混乱的bind调用变成一个漂亮而干净的程序:计算表达式!

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

If you take away one thing from all of this, let it be the following:如果你从这一切中拿走一件事,那就是:

"Variables" that are defined within a computation expression are not really "variables" at all, they only look like them, but in reality they're function parameters, and you have to treat them as such.在计算表达式中定义的“变量”根本不是真正的“变量”,它们只是看起来像它们,但实际上它们是 function 参数,你必须这样对待它们。 This means that every operation has to make sure to thread through whatever parameters it got from upstream.这意味着每个操作都必须确保遍历从上游获得的任何参数。 Otherwise those "variables" won't be available downstream.否则,这些“变量”将无法在下游使用。

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

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