F#表达式值未在计算表达式中定义 [英] F# saying value not defined in Computation Expression

查看:61
本文介绍了F#表达式值未在计算表达式中定义的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I've been working on a State Monad with F# Computation Expression and I'm trying to also utilize Custom Operations. 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

下面是错误的图片:

推荐答案

这一切与计算表达式的深层本质有关,根据您张贴在帖子上的标签,您必须已经了解它们是单子.

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.

什么是单子?这只是这种将计算链接在一起的模式的名称,将一个的结果作为参数传递给下一个,仅此而已.请参阅此答案,以获取有关示例的更全面的说明.在这里,我假设您知道 bind return 的工作原理,尤其是了解自己如何为 State 实施它们.

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.

什么是计算表达式?它们通常被称为"monad理解",这基本上意味着它们是monad的句法糖.实际上,这意味着它们是聪明的语法,最终会使 desugared 陷入一系列 bind return 调用中.

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.

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

Let's consider a simplified example without sleep:

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

此代码将对以下内容进行糖化处理:

This code would desugar into this:

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

看看这里发生了什么?在 getFood 之后的计算部分变成了一个函数,该函数将 food 作为参数.这就是 printfn 行如何获取 food 的值进行打印的方法-通过将其作为参数传递给函数.

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. 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.

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

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.

要查看会发生什么,让我们尝试:

To see what happens, let's try to eat:

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

这将被废除:

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

嗯...看看这里发生了什么? Eat 的第二个参数是 food ,但这在任何地方都没有定义!仅在该嵌套函数内部有效!这是您遇到错误的地方.

Hmm... See what happened here? The second parameter of Eat is food, but that's not defined anywhere! It's only valid inside that nested function! This is where you're getting your error.

因此,计算表达式具有特殊之处: ProjectionParameterAttribute .此处的投影"一词指的是投影".粗略的意思是变换",并且想法是这样的参数将是一个 function ,可以在到目前为止"计算的计算结果上调用该参数.提取其中的一部分.

So to deal with this, computation expressions have a special thing: 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.

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

In practice this means that if we annotate Eat like so:

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)
)

请注意嵌套函数如何调用 state.Return ,以便整个 Eat 的第一个参数的结果为 food .这样做是有目的的,以使中间变量可用于下一部分计算.这就是保持可变空间"的意思.

Notice how the nested function calls state.Return, so that the result of the whole Eat's first parameter is the value of 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".

然后注意 Eat 的第二个参数如何变成 fun x->x -表示它正在从 Eat 的第一个参数通过该 state返回的中间状态中提取 food 的值..

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 实际上可以调用该函数来获取 food 的值.

Now Eat can actually call that function to get at the value of food.

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

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

Note the parameter x - that comes from state, funneled into the lambda expression by State.bind. If you look at the type of Eat, you'll see that it became this:

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

意思是它需要进行状态计算以生成一些'a ,再加上一个从'a Food 的函数,然后返回状态计算不会产生任何结果(即 unit ).

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 (i.e. unit).

到目前为止,一切都很好.这样可以解决" 食物未定义"的问题.问题.

So far so good. This will fix the "food is not defined" problem.

但不是那么快!现在,您遇到了一个新问题.尝试重新引入 sleep :

But not so fast! Now you have a new problem. Try introducing sleep back in:

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

现在您遇到一个新错误: food 的类型应该是 Food ,但是这里的类型是 unit .

And now you get a new error: food was expected to have type Food, but here has type unit.

WTF正在这里?!

好吧,您只是在 Sleep 里面扔掉了 food ,仅此而已.

Well, you're just throwing away the food inside Sleep, that's all.

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

您会看到, Sleep 进行计算会产生 something ,然后继续丢弃该 something 并运行 sleepProgram ,这是产生 unit 的计算,所以 sleep 的结果就是这样.

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.

让我们看一下经过解密的代码:

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)
)

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

See how Sleep's result is the first parameter of Eat? That means Sleep needs to return a computation producing food, so that Eat's second parameter can have access to it. But Sleep doesn't. It returns the result of sleepProgram, which is a computation producing unit. So food is gone now.

Sleep 真正需要做的是首先运行 sleepProgram ,然后在其末尾链接另一个计算,该计算将返回原始 Sleep的结果的第一个参数.像这样:

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. Like this:

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

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

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!

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


如果您从所有这些东西中删除一件事,请遵循以下条件:

变量"在计算表达式中定义的变量"并不是真正的变量".根本上,它们只是看起来像它们,但实际上它们是函数参数,因此您必须将它们视为此类.这意味着每个操作都必须确保从上游获取的任何参数都经过线程处理.否则,那些变量"将被删除.不会在下游可用.

"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. 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.

这篇关于F#表达式值未在计算表达式中定义的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆