Common Queries and Commands Using MailboxProcessor

I think this question affects the same area, but I do not see how this can be applied to my situation. General response from agent / mail server processor?

Here is the background. I have some kind of state, let's just say, now it contains only a list of players. Could be more, for example. Games, etc. I also have an initial country that has no players.

type Player = {Name: string; Points: int} type State = {Players: Player list} let initialState = {Players = []} 

I have two types of messages that I need to deal with. Queries, which are functions that map a state to a value, but do not change state. For example. Return an int showing the highest point rating.

And commands that produce a new state, but can return a value. For example, add a new player to the collection and return the identifier or something else.

 type Message<'T> = | Query of (State -> 'T) | Command of (State -> 'T * State) 

And then we have a model that can respond to messages. But which, unfortunately, uses mutable state, I would prefer to use MailboxProcessor and the message loop.

 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 

Obviously, it would be nice if this State argument itself was general. But one step at a time.

All I was trying to replace was that the modified currentState using MailboxProcessor failed. The problem is the Generics and Static nature of F #, but I cannot find a way around this.

The following steps do not work, but it shows what I would like to do.

 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 
+8
f #
source share
2 answers

As Scott mentioned, the problem is that your Message<'T> is generic, but the way you use it restricts 'T only one type inside the agent body.

However, the agent really does not need to do anything with the value 'T It simply passes the result of the function (included in the message) to the asynchronous response channel (also included in the message). Thus, we can solve this by completely hiding the value of type 'T from the agent and making the message a value that simply carries the function:

 type Message = | Query of (State -> unit) | Command of (State -> State) 

You can even use only the State -> State function (with a query, which is a function that always returns the same state), but I wanted to keep the original structure.

Inside the agent, you can simply call the function for commands, switch to a new 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) 

An interesting bit is the members. They will be universal and will use PostAndAsyncReply to create a value of type AsyncReplyChannel<'T> . However, the scope of 'T can be limited by the body of functions, since now they will build Query or Command values, which themselves send a response directly to the newly created channel:

  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)) 

In fact, this is very similar to your original solution. We just needed to extract all the code related to the values โ€‹โ€‹of 'T from the agent body into general methods.

EDIT: Adding a version that is also common to the state:

 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)) 
+5
source share

The problem is that the generic Message<'T> bound to a specific type ( Player ) when type inference occurs on AddPlayer . Subsequent calls don't need to be int , bool , etc.

That is, it is common only when it is defined. When used, a particular model must have a specific type.

There are several solutions, but I think they are not very elegant.

My preferred approach would be to combine all possible query and command results, as shown below.

 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 {...} 
+4
source share

All Articles