使用MailboxProcessor的通用查询和命令

时间:2015-11-30 23:39:56

标签: f#

我认为这个问题涉及同一领域,但我看不出它如何适用于我的情况。 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

2 个答案:

答案 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 &lt;Fingerpainter.OpacityViewController: 0x79095110>  on &lt;Fingerpainter.DrawingViewController: 0x7b278000> which is already presenting &lt;Fingerpainter.BrushSizeViewController: 0x79573770>

答案 1 :(得分:4)

问题是当类型推断时,通用Message<'T>被绑定到特定类型(Player) 发生在AddPlayer上。后续调用要求'Tintbool等。

也就是说,它仅在定义时是通用的。在使用中,特定模型必须具有特定类型。

我认为有几种解决方案,但没有一种非常优雅。

我首选的方法是使用所有可能的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 {...}