我认为这个问题涉及同一领域,但我看不出它如何适用于我的情况。 Generic reply from agent/mailboxprocessor?
这是背景资料。我有一些状态,现在我只想说它只包含一个玩家列表。可能会有更多,例如游戏等我也有一个没有玩家的initialState。
type Player = {Name: string; Points: int}
type State = {Players: Player list}
let initialState = {Players = []}
我需要处理两种“消息”。 查询,是将状态映射到某个值但不更改状态的函数。例如。返回显示最高得分的int。
生成新状态的命令,但可以返回值。 E.g将新玩家添加到集合中,并返回id或其他任何内容。
type Message<'T> =
| Query of (State -> 'T)
| Command of (State -> 'T * State)
然后我们有一个可以响应消息的模型。但不幸的是,它使用了一个可变状态,我更喜欢使用MailboxProcessor和消息循环。
type Model(state: State) =
let mutable currentState = state
let HandleMessage (m: Message<'outp>) =
match m with
| Query q -> q currentState
| Command c ->
let n, s = c currentState
currentState <- s
n
member this.Query<'T> (q: State -> 'T) =
HandleMessage (Query q)
member this.Command<'T> (c: State -> 'T * State) =
HandleMessage (Command c)
// Query Methods
let HowMany (s: State) = List.length s.Players
let HasAny (s: State) = (HowMany s) > 0
let ShowAll (s: State) = s
// Command Methods
let AddPlayer (p: Player) (s: State) = (p, {s with Players = p::s.Players})
let model = new Model(initialState)
model.Command (AddPlayer {Name="Sandra"; Points=1000})
model.Query HasAny
model.Query HowMany
model.Query ShowAll
显然,如果State论证本身是通用的,那就太好了。但是一步一步。
我尝试用MailboxProcessor替换可变currentState的所有内容都失败了。问题在于F#的泛型和静态性质,但我无法找到解决方法。
以下内容不起作用,但它显示了我想要做的事情。
type Player = {Name: string; Points: int}
type State = {Players: Player list}
let initialState = {Players = []}
type Message<'T> =
| Query of (State -> 'T) * AsyncReplyChannel<'T>
| Command of (State -> 'T * State) * AsyncReplyChannel<'T>
type Model(state: State) =
let innerModel =
MailboxProcessor.Start(fun inbox ->
let rec messageLoop (state: State) =
async {
let! msg = inbox.Receive()
match (msg: Message<'outp>) with
| Query (q, replyChannel) ->
replyChannel.Reply(q state)
return! messageLoop state
| Command (c, replyChannel) ->
let result, newState = c state
replyChannel.Reply(result)
return! messageLoop(newState)
}
messageLoop initialState)
member this.Query<'T> (q: State -> 'T) =
innerModel.PostAndReply(fun chan -> Query(q , chan))
member this.Command<'T> (c: State -> 'T * State) =
innerModel.PostAndReply(fun chan -> Command(c, chan))
// Query Methods
let HowMany (s: State) = List.length s.Players
let HasAny (s: State) = (HowMany s) > 0
let ShowAll (s: State) = s
//// Command Methods
let AddPlayer (p: 'T) (s: State) = {s with Players = p::s.Players}
let model = new Model(initialState)
model.Command (AddPlayer {Name="Joe"; Points=1000})
model.Query HowMany
model.Query HasAny
model.Query ShowAll
答案 0 :(得分:6)
正如斯科特所提到的,问题是您的'T
类型是通用的,但它的使用方式将'T
限制为代理正文中的单个类型。
但是,代理并不真正需要对值'T
执行任何操作。它只是将函数(包含在消息中)的结果传递给异步回复通道(也包含在消息中)。因此,我们可以通过从代理中完全隐藏类型type Message =
| Query of (State -> unit)
| Command of (State -> State)
的值并使消息成为仅包含函数的值来解决此问题:
State -> State
你甚至可以只使用一个函数type Model(state: State) =
let innerModel =
MailboxProcessor<Message>.Start(fun inbox ->
let rec messageLoop (state: State) =
async {
let! msg = inbox.Receive()
match msg with
| Query q ->
q state
return! messageLoop state
| Command c ->
let newState = c state
return! messageLoop(newState)
}
messageLoop initialState)
(查询是一个总是返回相同状态的函数),但我想保留原始结构。
在代理内部,您现在可以只调用该函数,对于命令,切换到新状态:
PostAndAsyncReply
有趣的是成员。它们是通用的,仍然使用AsyncReplyChannel<'T>
来创建'T
类型的值。但是,Query
的范围可以限制在函数体中,因为它们现在将构造Command
或 member this.Query<'T> (q: State -> 'T) =
innerModel.PostAndReply(fun chan -> Query(fun state ->
let res = q state
chan.Reply(res)))
member this.Command<'T> (c: State -> 'T * State) =
innerModel.PostAndReply(fun chan -> Command(fun state ->
let res, newState = c state
chan.Reply(res)
newState))
值,这些值自己将回复直接发布到我们刚刚创建的频道:
'T
事实上,这与您原来的解决方案非常相似。我们只需要将代理体中处理type Message<'TState> =
| Query of ('TState -> unit)
| Command of ('TState -> 'TState)
type Model<'TState>(initialState: 'TState) =
let innerModel =
MailboxProcessor<Message<'TState>>.Start(fun inbox ->
let rec messageLoop (state: 'TState) =
async {
let! msg = inbox.Receive()
match msg with
| Query q ->
q state
return! messageLoop state
| Command c ->
let newState = c state
return! messageLoop(newState)
}
messageLoop initialState)
member this.Query<'T> (q: 'TState -> 'T) =
innerModel.PostAndReply(fun chan -> Query(fun state ->
let res = q state
chan.Reply(res)))
member this.Command<'T> (c: 'TState -> 'T * 'TState) =
innerModel.PostAndReply(fun chan -> Command(fun state ->
let res, newState = c state
chan.Reply(res)
newState))
值的所有代码提取到泛型方法中。
编辑:添加一个通用状态的版本:
Warning: Attempt to present <Fingerpainter.OpacityViewController: 0x79095110> on <Fingerpainter.DrawingViewController: 0x7b278000> which is already presenting <Fingerpainter.BrushSizeViewController: 0x79573770>
答案 1 :(得分:4)
问题是当类型推断时,通用Message<'T>
被绑定到特定类型(Player
)
发生在AddPlayer
上。后续调用要求'T
为int
,bool
等。
也就是说,它仅在定义时是通用的。在使用中,特定模型必须具有特定类型。
我认为有几种解决方案,但没有一种非常优雅。
我首选的方法是使用所有可能的Query和Command结果的联合,如下所示。
type Player = {Name: string; Points: int}
type State = {Players: Player list}
// I've been overly explicit here!
// You could just use a choice of | Int | Bool | State, etc)
type QueryResult =
| HowMany of int
| HasAny of bool
| ShowAll of State
type CommandResult =
| Player of Player
type Message =
| Query of (State -> QueryResult) * AsyncReplyChannel<QueryResult>
| Command of (State -> CommandResult * State) * AsyncReplyChannel<CommandResult>
type Model(initialState: State) =
let agent = MailboxProcessor.Start(fun inbox ->
let rec messageLoop (state: State) =
async {
let! msg = inbox.Receive()
match msg with
| Query (q, replyChannel) ->
let result = q state
replyChannel.Reply(result)
return! messageLoop state
| Command (c, replyChannel) ->
let result, newState = c state
replyChannel.Reply(result)
return! messageLoop(newState)
}
messageLoop initialState)
member this.Query queryFunction =
agent.PostAndReply(fun chan -> Query(queryFunction, chan))
member this.Command commandFunction =
agent.PostAndReply(fun chan -> Command(commandFunction, chan))
// ===========================
// test
// ===========================
// Query Methods
// Note that the return values have to be lifted to QueryResult
let howMany (s: State) = HowMany (List.length s.Players)
let hasAny (s: State) = HasAny (List.length s.Players > 0)
let showAll (s: State) = ShowAll s
// Command Methods
// Note that the return values have to be lifted to CommandResult
let addPlayer (p: Player) (s: State) = (Player p, {s with Players = p::s.Players})
// setup a model
let initialState = {Players = []}
let model = new Model(initialState)
model.Command (addPlayer {Name="Sandra"; Points=1000})
model.Query hasAny // HasAny true
model.Query howMany // HowMany 1
model.Query showAll // ShowAll {...}