F#说值未在计算表达式中定义
f#
我一直在研究带有 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
这是错误的图片:
回答
这一切都与计算表达式的深层性质有关,根据您在帖子中放置的标签判断,您必须已经了解monads。
什么是单子?它只是这种将计算链接在一起的模式的名称,将一个计算的结果作为参数传递给下一个,仅此而已。有关示例的更全面的解释,请参阅此答案。下面我就假设你知道如何bind和return工作,尤其是看到你是如何实现它们State自己。
什么是计算表达式?它们是您通常所说的“monad 理解”,这基本上意味着它们是monad 的语法糖。实际上,这意味着它们是聪明的语法,最终会被分解为一系列的bindandreturn调用。
让我们考虑一个没有 的简化示例sleep:
state {
let! food = getFood
printfn $"{food}"
}
此代码将脱糖为:
state.Bind(
getFood,
(fun food ->
printfn "${food}"
state.Return ()
)
)
看看这里发生了什么?之后的计算部分getFood变成了一个函数,这个函数food作为一个参数。这就是该printfn行获取foodto print值的方式 - 凭借它作为参数传递给函数。
但是,自定义操作的工作方式略有不同。当编译器遇到自定义操作时,它会获取自定义操作之前的整个表达式(Bind调用序列),并将整个内容作为参数传递给自定义操作。
为了看看会发生什么,让我们尝试eat:
state {
let! food = getFood
printfn $"{food}"
eat food
}
这将被脱糖为:
state.Eat(
state.Bind(
getFood,
(fun food ->
printfn $"{food}"
state.Return food
)
),
food
)
嗯……看看这里发生了什么?的第二个参数Eatis food,但没有在任何地方定义!它只在嵌套函数内有效!这是您遇到错误的地方。
所以为了解决这个问题,计算表达式有一个特殊的东西:ProjectionParameterAttribute. 这里的“投影”一词粗略地表示“转换”,其想法是这样的参数将是一个函数,可以在“到目前为止”计算的计算结果上调用它以提取它的某些部分。
在实践中,这意味着如果我们这样注释Eat:
member this.Eat (state:State<_,PlanAcc>, [<ProjectionParameter>] food) =
那么上面例子的脱糖就变成这样了:
state.Eat(
state.Bind(
getFood,
(fun food ->
printfn $"{food}"
state.Return(food)
)
),
(fun x -> x)
)
注意嵌套函数是如何调用的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
请注意参数x- 来自state,通过 汇集到 lambda 表达式中State.bind。如果您查看 的类型Eat,您会发现它变成了这样:
Eat : State<'a, StateAcc> * ('a -> Food) -> State<unit, StateAcc>
这意味着它需要一个产生 some 的状态计算'a,加上一个来自'ato的函数Food,它返回一个不产生任何东西的状态计算(即unit)。
到现在为止还挺好。这将解决“food未定义”问题。
但没那么快!现在你有一个新的问题。尝试引入sleep:
state {
let! food = getFood
printfn $"{food}"
sleep 2
eat food
}
现在你得到一个新的错误: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.
你看,Sleep进行一个计算产生的东西,然后扔掉那个东西然后运行sleepProgram,这是一个计算产生的unit,所以这就是结果sleep。
让我们看看脱糖后的代码:
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现在没了。
什么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
但这太丑了,不是吗?幸运的是,我们有一个方便的编译器功能可以将这些混乱的bind调用变成一个漂亮而干净的程序:计算表达式!
member this.Sleep (st:State<_,PlanAcc>, duration) =
printfn $"Sleep"
state {
let! x = st
do! sleepProgram duration
return x
}
如果你从这一切中拿走一件事,让它成为以下内容:
在计算表达式中定义的“变量”根本不是真正的“变量”,它们只是看起来像它们,但实际上它们是函数参数,您必须这样对待它们。这意味着每个操作都必须确保遍历从上游获得的任何参数。否则,这些“变量”将无法在下游使用。
- Glad I could help. I do sometimes adopt SO answers to blog posts (check out [our blog](https://medium.com/collegevine-product)), but it's not as simple as copy&paste. A blog post comes at you "out of the blue", so it needs to explain the problem before offering the solution. And here on SO you already did the problem part for me.
- This answer is outrageously awesome. Thank you! This should be a blogpost in of itself.
- You will be happy to know that I was able to successfully update the `Sleep` method to also use `[<ProjectionParameter>]` and use values declared earlier in the CE.