F# 说值未在计算表达式中定义
F# saying value not defined in Computation Expression
我一直在使用 F# Computation Expression 开发 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
这是错误的图片:
这一切都与计算表达式的深层性质有关,根据您在 post 上的标签判断,您必须已经了解 monads .
什么是单子?它只是这种将计算链接在一起的模式的名称,将一个计算的结果作为参数传递给下一个,仅此而已。请参阅 以获取更全面的示例说明。在这里,我假设您知道 bind
和 return
是如何工作的,尤其是看看您是如何为 State
自己实现它们的。
什么是计算表达式?它们就是您通常所说的“单子理解”,这基本上意味着它们是单子的语法糖。实际上,这意味着它们是巧妙的语法,最终 脱糖 到一系列 bind
和 return
调用。
让我们考虑一个没有 sleep
的简化示例:
state {
let! food = getFood
printfn $"{food}"
}
此代码将脱糖为:
state.Bind(
getFood,
(fun food ->
printfn "${food}"
state.Return ()
)
)
看到这里发生了什么? getFood
之后的计算部分变成了一个函数,这个函数以food
作为参数。这就是 printfn
行获取要打印的 food
值的方式——因为它被作为参数传递给函数。
但是,自定义操作的工作方式略有不同。当编译器遇到自定义操作时,它会获取自定义操作之前出现的整个表达式(Bind
调用序列),并将整个内容作为参数传递给自定义操作。
要看看会发生什么,让我们尝试 eat
:
state {
let! food = getFood
printfn $"{food}"
eat food
}
这将被脱糖为:
state.Eat(
state.Bind(
getFood,
(fun food ->
printfn $"{food}"
state.Return food
)
),
food
)
嗯...看到这里发生了什么吗? Eat
的第二个参数是 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
的值。这是有目的的,以使中间变量可用于计算的下一部分。这就是“维护变量space”的意思。
然后注意 Eat
的第二个参数如何变成 fun x -> x
- 这意味着它从 return 编辑的中间状态中提取 food
的值Eat
的第一个参数通过 state.Return
.
现在 Eat
实际上可以调用该函数来获取 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
的类型,你会发现它变成了这样:
Eat : State<'a, StateAcc> * ('a -> Food) -> State<unit, StateAcc>
意味着它需要一个状态计算产生一些 'a
,加上一个从 'a
到 Food
的函数,它 return 是一个什么都不产生的状态计算(即unit
).
到目前为止一切顺利。这将修复“food
未定义”问题。
但没那么快!现在你有一个新问题。尝试在 sleep
中引入 sleep
:
state {
let! food = getFood
printfn $"{food}"
sleep 2
eat food
}
现在你得到一个新的错误:food
应该有类型 Food
,但这里有类型 unit
.
这是怎么回事?!
嗯,你只是把Sleep
里面的food
扔掉了,就这样。
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
的结果。
我们来看脱糖后的代码:
state.Eat(
state.Sleep(
state.Bind(
getFood,
(fun food ->
printfn $"{food}"
state.Return food
)
),
2
),
(fun x -> x)
)
看看Sleep
的结果怎么是Eat
的第一个参数?这意味着 Sleep
需要 return 计算产生 food
,以便 Eat
的第二个参数可以访问它。但是 Sleep
没有。它 return 是 sleepProgram
的结果,这是一个产生 unit
的计算。所以 food
现在不见了。
Sleep
真正需要做的是首先运行sleepProgram
,然后到它的末尾链接另一个计算return原来的结果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
}
如果你从这一切中拿走一件事,那就是:
在计算表达式中定义的“变量”根本不是真正的“变量”,它们只是看起来像它们,但实际上它们是函数参数,你必须这样对待它们。这意味着每个操作都必须确保通过它从上游获得的任何参数。否则这些“变量”将无法在下游使用。
我一直在使用 F# Computation Expression 开发 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
这是错误的图片:
这一切都与计算表达式的深层性质有关,根据您在 post 上的标签判断,您必须已经了解 monads .
什么是单子?它只是这种将计算链接在一起的模式的名称,将一个计算的结果作为参数传递给下一个,仅此而已。请参阅 bind
和 return
是如何工作的,尤其是看看您是如何为 State
自己实现它们的。
什么是计算表达式?它们就是您通常所说的“单子理解”,这基本上意味着它们是单子的语法糖。实际上,这意味着它们是巧妙的语法,最终 脱糖 到一系列 bind
和 return
调用。
让我们考虑一个没有 sleep
的简化示例:
state {
let! food = getFood
printfn $"{food}"
}
此代码将脱糖为:
state.Bind(
getFood,
(fun food ->
printfn "${food}"
state.Return ()
)
)
看到这里发生了什么? getFood
之后的计算部分变成了一个函数,这个函数以food
作为参数。这就是 printfn
行获取要打印的 food
值的方式——因为它被作为参数传递给函数。
但是,自定义操作的工作方式略有不同。当编译器遇到自定义操作时,它会获取自定义操作之前出现的整个表达式(Bind
调用序列),并将整个内容作为参数传递给自定义操作。
要看看会发生什么,让我们尝试 eat
:
state {
let! food = getFood
printfn $"{food}"
eat food
}
这将被脱糖为:
state.Eat(
state.Bind(
getFood,
(fun food ->
printfn $"{food}"
state.Return food
)
),
food
)
嗯...看到这里发生了什么吗? Eat
的第二个参数是 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
的值。这是有目的的,以使中间变量可用于计算的下一部分。这就是“维护变量space”的意思。
然后注意 Eat
的第二个参数如何变成 fun x -> x
- 这意味着它从 return 编辑的中间状态中提取 food
的值Eat
的第一个参数通过 state.Return
.
现在 Eat
实际上可以调用该函数来获取 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
的类型,你会发现它变成了这样:
Eat : State<'a, StateAcc> * ('a -> Food) -> State<unit, StateAcc>
意味着它需要一个状态计算产生一些 'a
,加上一个从 'a
到 Food
的函数,它 return 是一个什么都不产生的状态计算(即unit
).
到目前为止一切顺利。这将修复“food
未定义”问题。
但没那么快!现在你有一个新问题。尝试在 sleep
中引入 sleep
:
state {
let! food = getFood
printfn $"{food}"
sleep 2
eat food
}
现在你得到一个新的错误:food
应该有类型 Food
,但这里有类型 unit
.
这是怎么回事?!
嗯,你只是把Sleep
里面的food
扔掉了,就这样。
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
的结果。
我们来看脱糖后的代码:
state.Eat(
state.Sleep(
state.Bind(
getFood,
(fun food ->
printfn $"{food}"
state.Return food
)
),
2
),
(fun x -> x)
)
看看Sleep
的结果怎么是Eat
的第一个参数?这意味着 Sleep
需要 return 计算产生 food
,以便 Eat
的第二个参数可以访问它。但是 Sleep
没有。它 return 是 sleepProgram
的结果,这是一个产生 unit
的计算。所以 food
现在不见了。
Sleep
真正需要做的是首先运行sleepProgram
,然后到它的末尾链接另一个计算return原来的结果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
}
如果你从这一切中拿走一件事,那就是:
在计算表达式中定义的“变量”根本不是真正的“变量”,它们只是看起来像它们,但实际上它们是函数参数,你必须这样对待它们。这意味着每个操作都必须确保通过它从上游获得的任何参数。否则这些“变量”将无法在下游使用。