在 F# 中使用不可变记录的状态机
State Machine using immutable Records in F#
我对 F# 中的状态机有以下定义:
type MyEvent = Event1 | Event2 | Event3
type MachineState<'event when 'event:comparison> =
{
Transitions: Map<'event, MachineState<'event>>
Data: int
//...other State stuff, like parent state, entry/exit actions etc
}
static member Default = {Transitions=Map.empty}
//simple helpers
let on event endState state =
{state with Transitions = state.Transitions.Add(event, endState)}
let withData data state = {state with Data = data}
我的想法是,给定一个状态和一个事件,我将在转换映射中搜索事件键,如果找到,我将 return 新状态,否则将 return当前的。
状态定义如下:
let rec StateA =
MachineState<_>.Default
|> on Event1 StateB
|> withData 5
and StateB =
MachineState<_>.Default
|> on Event2 StateC
|> withData -999
and StateC =
MachineState<_>.Default
//|> on Event3 StateA //This actually gives a runtime error
|> withData 84
这给了我两个问题:
一个错误 FS0031,表示 StateA 是它自己定义的一部分,还有一个警告,warn40,表示将在运行时评估对象的初始化可靠性。
我可以通过将所有内容包装在 lazy 中来修复错误:
...Transitions: Map<'event, Lazy<MachineState<'event>>>...
let rec StateA =
lazy (MachineState<_>.Default
|> on Event1 StateB)
and StateB =
lazy (MachineState<_>.Default
|> on Event2 StateC)
and StateC =
lazy (MachineState<_>.Default
|> on Event3 StateA)
这并没有解决警告问题,感觉有点勉强。
这是解决此问题的最佳方法吗?有没有更好的方法来处理不可变的递归结构?或者,更具体地说,实施不可变的 HFSM?
此 fiddle 包含一个 运行 示例:https://dotnetfiddle.net/TjjeBz
最好将 State
建模为简单的数据项。
type State =
| A
| B
| C
type Event =
| Event1
| Event2
| Event3
type StateMachine<'state, 'ev when 'state : comparison and 'ev : comparison> =
{
Transitions : Map<'state, Map<'ev, 'state>>
}
with
static member Default
with get () =
{
Transitions = Map.empty
}
module StateMachine =
let addTransition startState ev endState sm =
let m =
sm.Transitions
|> Map.tryFind startState
|> Option.defaultValue Map.empty
|> Map.add ev endState
{
sm with
Transitions =
sm.Transitions
|> Map.add startState m
}
let tryTransition state ev sm =
sm.Transitions
|> Map.tryFind state
|> Option.defaultValue Map.empty
|> Map.tryFind ev
let myStateMachine : StateMachine<State, Event> =
StateMachine<State, Event>.Default
|> StateMachine.addTransition A Event1 B
|> StateMachine.addTransition B Event2 C
|> StateMachine.addTransition C Event3 A
printfn "%A" (myStateMachine |> StateMachine.tryTransition A Event1)
// Some B
printfn "%A" (myStateMachine |> StateMachine.tryTransition A Event2)
// None
我使用 Map
来存储转换,因为它们提供更有效的查找,但您可以改用 List
。
如果您想在转换期间触发副作用,我建议将它们保留在状态表示之外。
例如:
let transitionAction previousState nextState =
match previousState, nextState with
| (A, B) ->
async {
printfn "Launching the missiles... "
do! launchMissiles
printfn "Game over."
}
| (_, _) ->
async {
() // Do nothing
}
由于 'state
可以是任何类型,您也可以向其附加任意数据:
type City =
| London
| NewYork
| Tokyo
type Fuel = int
type State = City * Fuel
这里的问题是您想要构造一个不可变的递归值 - 一个在内部某处包含对自身的引用的对象。这在函数式语言中很难做到,因为对象是不可变的。 F# 实际上可以在某些有限的情况下执行此操作。
在您的情况下,如果您仅使用原始值构造函数,则可以使内置的 F# 递归初始化工作 - 即不调用任何函数。我不得不用普通 list
替换转换列表中的 Map
,但这有效:
type MyEvent = Event1 | Event2 | Event3
type MachineState<'event when 'event:comparison> =
{ Transitions: ('event * MachineState<'event>) list
Data: int }
static member Default = {Transitions=[]; Data=1}
let rec StateA =
{ Transitions = [ Event1, StateB]
Data = 5 }
and StateB =
{ Transitions = [ Event1, StateC]
Data = -999 }
and StateC =
{ Transitions = [ Event1, StateA]
Data = 84 }
如果您想保留递归结构,但使用自定义函数对其进行初始化,那么您将需要在数据结构的某处有某种 Lazy
。您的解决方法与任何其他选项一样有效。我可能会使整个 Map<..>
变得懒惰,这可能会为您提供更好的构造语法:
type MachineState<'event when 'event:comparison> =
{ Transitions: Lazy<Map<'event, MachineState<'event>>>
Data: int }
static member Default = {Transitions=lazy Map.empty; Data=1}
let on event (endState:Lazy<_>) state =
{state with Transitions = lazy state.Transitions.Value.Add(event, endState.Value)}
let withData data state = {state with Data = data}
let rec StateA =
MachineState<_>.Default
|> on Event1 (lazy StateB)
|> withData 5
and StateB =
MachineState<_>.Default
|> on Event2 (lazy StateC)
|> withData -999
and StateC =
MachineState<_>.Default
|> on Event3 (lazy StateA)
|> withData 84
我对 F# 中的状态机有以下定义:
type MyEvent = Event1 | Event2 | Event3
type MachineState<'event when 'event:comparison> =
{
Transitions: Map<'event, MachineState<'event>>
Data: int
//...other State stuff, like parent state, entry/exit actions etc
}
static member Default = {Transitions=Map.empty}
//simple helpers
let on event endState state =
{state with Transitions = state.Transitions.Add(event, endState)}
let withData data state = {state with Data = data}
我的想法是,给定一个状态和一个事件,我将在转换映射中搜索事件键,如果找到,我将 return 新状态,否则将 return当前的。 状态定义如下:
let rec StateA =
MachineState<_>.Default
|> on Event1 StateB
|> withData 5
and StateB =
MachineState<_>.Default
|> on Event2 StateC
|> withData -999
and StateC =
MachineState<_>.Default
//|> on Event3 StateA //This actually gives a runtime error
|> withData 84
这给了我两个问题: 一个错误 FS0031,表示 StateA 是它自己定义的一部分,还有一个警告,warn40,表示将在运行时评估对象的初始化可靠性。
我可以通过将所有内容包装在 lazy 中来修复错误:
...Transitions: Map<'event, Lazy<MachineState<'event>>>...
let rec StateA =
lazy (MachineState<_>.Default
|> on Event1 StateB)
and StateB =
lazy (MachineState<_>.Default
|> on Event2 StateC)
and StateC =
lazy (MachineState<_>.Default
|> on Event3 StateA)
这并没有解决警告问题,感觉有点勉强。
这是解决此问题的最佳方法吗?有没有更好的方法来处理不可变的递归结构?或者,更具体地说,实施不可变的 HFSM?
此 fiddle 包含一个 运行 示例:https://dotnetfiddle.net/TjjeBz
最好将 State
建模为简单的数据项。
type State =
| A
| B
| C
type Event =
| Event1
| Event2
| Event3
type StateMachine<'state, 'ev when 'state : comparison and 'ev : comparison> =
{
Transitions : Map<'state, Map<'ev, 'state>>
}
with
static member Default
with get () =
{
Transitions = Map.empty
}
module StateMachine =
let addTransition startState ev endState sm =
let m =
sm.Transitions
|> Map.tryFind startState
|> Option.defaultValue Map.empty
|> Map.add ev endState
{
sm with
Transitions =
sm.Transitions
|> Map.add startState m
}
let tryTransition state ev sm =
sm.Transitions
|> Map.tryFind state
|> Option.defaultValue Map.empty
|> Map.tryFind ev
let myStateMachine : StateMachine<State, Event> =
StateMachine<State, Event>.Default
|> StateMachine.addTransition A Event1 B
|> StateMachine.addTransition B Event2 C
|> StateMachine.addTransition C Event3 A
printfn "%A" (myStateMachine |> StateMachine.tryTransition A Event1)
// Some B
printfn "%A" (myStateMachine |> StateMachine.tryTransition A Event2)
// None
我使用 Map
来存储转换,因为它们提供更有效的查找,但您可以改用 List
。
如果您想在转换期间触发副作用,我建议将它们保留在状态表示之外。
例如:
let transitionAction previousState nextState =
match previousState, nextState with
| (A, B) ->
async {
printfn "Launching the missiles... "
do! launchMissiles
printfn "Game over."
}
| (_, _) ->
async {
() // Do nothing
}
由于 'state
可以是任何类型,您也可以向其附加任意数据:
type City =
| London
| NewYork
| Tokyo
type Fuel = int
type State = City * Fuel
这里的问题是您想要构造一个不可变的递归值 - 一个在内部某处包含对自身的引用的对象。这在函数式语言中很难做到,因为对象是不可变的。 F# 实际上可以在某些有限的情况下执行此操作。
在您的情况下,如果您仅使用原始值构造函数,则可以使内置的 F# 递归初始化工作 - 即不调用任何函数。我不得不用普通 list
替换转换列表中的 Map
,但这有效:
type MyEvent = Event1 | Event2 | Event3
type MachineState<'event when 'event:comparison> =
{ Transitions: ('event * MachineState<'event>) list
Data: int }
static member Default = {Transitions=[]; Data=1}
let rec StateA =
{ Transitions = [ Event1, StateB]
Data = 5 }
and StateB =
{ Transitions = [ Event1, StateC]
Data = -999 }
and StateC =
{ Transitions = [ Event1, StateA]
Data = 84 }
如果您想保留递归结构,但使用自定义函数对其进行初始化,那么您将需要在数据结构的某处有某种 Lazy
。您的解决方法与任何其他选项一样有效。我可能会使整个 Map<..>
变得懒惰,这可能会为您提供更好的构造语法:
type MachineState<'event when 'event:comparison> =
{ Transitions: Lazy<Map<'event, MachineState<'event>>>
Data: int }
static member Default = {Transitions=lazy Map.empty; Data=1}
let on event (endState:Lazy<_>) state =
{state with Transitions = lazy state.Transitions.Value.Add(event, endState.Value)}
let withData data state = {state with Data = data}
let rec StateA =
MachineState<_>.Default
|> on Event1 (lazy StateB)
|> withData 5
and StateB =
MachineState<_>.Default
|> on Event2 (lazy StateC)
|> withData -999
and StateC =
MachineState<_>.Default
|> on Event3 (lazy StateA)
|> withData 84