2015-11-30 7 views
8

Ich denke, diese Frage berührt den gleichen Bereich, aber ich kann nicht sehen, wie es auf meine Situation angewendet werden kann. Generic reply from agent/mailboxprocessor?Generische Abfragen und Befehle mit einem MailboxProcessor

Hier ist der Hintergrund. Ich habe einen Zustand, lass uns einfach sagen, dass es nur eine Liste von Spielern enthält. Es könnte mehr geben, z.B. Spiele usw. Ich habe auch einen initialState, der keine Spieler hat.

Ich habe zwei Arten von "Nachrichten", mit denen ich mich befassen muss. Abfragen, bei denen es sich um Funktionen handelt, die den Status einem Wert zuordnen, den Status jedoch nicht ändern. Z.B. Gebe einen Int zurück, der den höchsten Punktestand zeigt.

Und Befehle, die einen neuen Zustand erzeugen, aber einen Wert zurückgeben können. ZB Geben Sie einen neuen Spieler zur Sammlung und geben Sie eine ID oder was auch immer zurück.

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

Und dann haben wir ein Modell, das auf Nachrichten reagieren kann. Aber die leider einen veränderlichen Zustand verwendet, würde ich lieber einen MailboxProcessor und eine Nachrichtenschleife verwenden.

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 

Offensichtlich wäre es nett, wenn dieses State Argument selbst generisch wäre. Aber ein Schritt nach dem anderen.

Alles, was ich versucht habe, diesen veränderlichen currentState durch einen MailboxProcessor zu ersetzen, ist fehlgeschlagen. Das Problem ist mit den Generics und der statischen Natur von F #, aber ich kann keinen Weg finden.

Das Folgende funktioniert nicht, aber es zeigt, was ich tun möchte.

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 

Antwort

5

Wie Scott erwähnt, ist das Problem, dass Ihre Message<'T> Typ generisch ist, aber die Art und Weise schränkt 'T innerhalb des Körpers des Mittels nur ein einziger Typ verwendet wird.

Allerdings braucht der Agent nicht wirklich müssen etwas mit dem Wert 'T tun. Es übergibt einfach das Ergebnis der Funktion (in der Nachricht enthalten) an den asynchronen Antwortkanal (der ebenfalls in der Nachricht enthalten ist).So können wir dies lösen, indem sie vollständig den Wert vom Typ 'T vom Agenten versteckt und einen Wert machen Nachricht, die nur eine Funktion trägt:

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

Sie auch nur eine Funktion State -> State (mit Abfrage verwenden, könnte eine Funktion ist, dass gibt immer den gleichen Zustand zurück), aber ich wollte die ursprüngliche Struktur behalten.

Im Inneren des Agenten können Sie jetzt rufen nur die Funktion und für Befehle, wechseln Sie in den neuen Staat:

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) 

Das interessante Bit sind die Mitglieder. Sie werden generisch sein und weiterhin PostAndAsyncReply verwenden, um einen Wert vom Typ AsyncReplyChannel<'T> zu erstellen. Allerdings kann der Umfang der 'T an den Körper der Funktionen eingeschränkt werden, weil sie jetzt Query oder Command Werte konstruieren, die sich die Antwort direkt an den Kanal veröffentlichen wir gerade erstellt haben:

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 der Tat ist dies sehr ähnlich zu Ihrer ursprünglichen Lösung. Wir mussten einfach den gesamten Code, der sich auf 'T Werte bezieht, aus dem Hauptteil des Agenten in die generischen Methoden extrahieren.

EDIT: Hinzufügen einer Version, die auch generische über den Zustand:

+0

Tomas, genau das habe ich versucht. Ich konnte sehen, dass der Agent die Funktionen übergeben wurde, so dass er nichts tun musste, außer das Ergebnis wieder zu bekommen. Deshalb habe ich so lange damit verbracht, es war wirklich nervig zu denken, dass ein generischer Ansatz vielleicht nicht möglich ist. –

+0

Habe ich recht zu denken, dass es zu weit ist, den Staat selbst zu einem Generikum zu machen? z.B. Etwas wie. type Modell <'S> (state: 'S) = ... –

+0

@RichardDalton Die generischen Zustand sollte vollständig funktionieren. Sie müssen nur die 'Message'- und' Model'-Typen generisch machen und auch 'initialState' als Konstruktor-Parameter an das' Model' übergeben, aber dann sollte es gut funktionieren! –

4

Das Problem ist, dass die generische Message<'T> auf einen bestimmten Typ gebunden wird (Player), wenn Typinferenz auf AddPlayer passiert. Die nachfolgenden Aufrufe erfordern 'T als int, bool usw.

Das heißt, es ist nur generisch, wenn definiert. Im Gebrauch muss ein insbesondere Modell einen insbesondere Typ haben.

Es gibt ein paar Lösungen, aber keine sehr elegant, denke ich.

Meine bevorzugte Vorgehensweise wäre eine Vereinigung aller möglichen Abfrage- und Befehlsergebnisse, wie unten gezeigt.

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 {...} 
+0

und .... Tomas Antwort ist besser als meine! – Grundoon

+1

Ich würde diese Antwort nicht ausschließen. Wenn ich einen bestimmten Agenten für eine bestimmte Aufgabe erstellen würde, wäre dies definitiv der Ansatz, den ich wählen würde. Und ich bin noch nicht vollständig verkauft, als eine generische ist besser. Aber ich wollte wirklich sehen, wie man einen baut. –

+0

Ich denke auch, dass das Hinzufügen von Operationen zum Domänenmodell in bestimmten Szenarien sehr sinnvoll wäre. Wenn es eine geschlossene Reihe von Operationen gäbe, wäre dies definitiv ein guter Weg, dies zu tun. –

Verwandte Themen