2014-07-07 13 views
13

Die Go-Sprache hat eine select-Anweisung, die verwendet werden kann, um mehrere Kanäle abzufragen und eine bestimmte Aktion durchzuführen, je nachdem, welcher Kanal zuerst nicht leer ist.Wie implementieren Sie das Äquivalent von Go's Select-Anweisung für Haskell STM-Kanäle?

z.

select { 
    case a := <- chanA: 
    foo(a) 
    case b := <- chanB: 
    baz(b) 
    case c := <- chanC: 
    bar(c) 
} 

Dies wird warten, bis entweder chanA, chanB oder chanC nicht leer ist, dann, wenn zum Beispiel chanB nicht leer ist, ist es aus chanB und speichert das Ergebnis in b, dann ruft baz(b) lesen. Eine default:-Klausel kann auch hinzugefügt werden, was bedeutet, dass die select-Anweisung nicht auf die Kanäle wartet und stattdessen die default-Klausel ausführt, wenn alle Kanäle leer sind.

Was wäre der beste Weg, um so etwas für STM TChan s in Haskell zu implementieren? Es könnte naiv durch eine if-else-Kette gemacht werden: Überprüfen, ob jeder chan isEmptyChan, und wenn es nicht leer ist, dann lesen von ihm und die entsprechende Funktion aufrufen, oder sonst retry aufrufen, wenn alle Kanäle leer sind. Ich habe mich gefragt, ob es eine elegantere/elegantere Art geben würde, dies zu tun?

Beachten Sie, dass die select-Anweisung von Go auch send-Anweisungen in ihren Fällen enthalten kann und eine send-Anweisung nur vervollständigt, wenn ihr Kanal leer ist. Es wäre großartig, wenn diese Funktionalität auch dupliziert werden könnte, obwohl ich nicht sicher bin, ob es einen eleganten Weg dazu geben würde.

Nur wenig verwandt, aber etwas, das ich gerade bemerkt, und ich bin nicht sicher, wo es schreiben: es auf der Control.Monad.STM Seite in der Beschreibung ein Tippfehler ist für retry:

„Die Implementierung der Thread, bis ein Block kann von den TVars, die es gelesen hat, war udpated. "

+1

Sie könnten sich "r Ass "aus" Control.Concurrent.Async ". –

+4

Es ist erwähnenswert, dass go nicht die erste verfügbare Aktion ausführt, sondern eine verfügbare, zufällig ausgewählte. Es wird Kanäle nicht verhungern, nur weil sie später oder unglücklich im Auswahlpfad definiert sind. – Dustin

+0

Das ist völlig anders als Gos 'select'. Kanäle in Go sind im Gegensatz zu 'TChan' begrenzt (wodurch sie tatsächlich nützlich sind) und' select' kann mit Sendeoperationen verwendet werden. – rightfold

Antwort

5

Starvation vermeiden

foreverK :: (a -> m a) -> a -> m() 
foreverK loop = go 
where go = loop >=> go 

-- Existential, not really required, but feels more like the Go version 
data ChanAct = Action (TChan a) (a -> STM()) 

perform :: STM() 
perform (Action c a) = readTChan c >>= a 

foreverSelectE :: [ChanAct] -> STM() 
foreverSelectE = foreverSelect . map perform 

foreverSelect :: [STM()] -> STM() 
foreverSelect = foreverK $ \xs -> first xs >> return (rotate1 xs) 

-- Should only be defined for non-empty sequences, but return() is an okay default. 
-- Will NOT block the thread, but might do nothing. 
first :: [STM()] -> STM() 
first = foldr orElse (return()) 

-- Should only be defined for non-empty sequences, really. 
-- Also, using a list with O(1) viewL and snoc could be better. 
rotate1 :: [a] -> [a] 
rotate1 [] = [] 
rotate1 (h:t) = t ++ [h] 

example = foreverSelectE 
    [ Action chanA foo 
    , Action charB baz 
    , Action chanC bar 
    ] 

für immer zu vermeiden, könnten Sie stattdessen haben ein mkSelect :: [STM()] -> STM (STM()), dass "versteckt" ein TVar [STM()] und dreht es jeder sie verwendet wird, wie:

example1 :: STM() 
example1 = do 
    select <- mkSelect [actions] -- Just set-up 
    stuff1 
    select -- does one of the actions 
    stuff2 
    select -- does one of the actions 

main = OpenGL.idleCallback $= atomically example1 

Wenn Sie diese Technik erweitern, können Sie eine Auswahl treffen, die gemeldet wird, wenn sie eine Aktion ausgeführt hat oder welche Aktion ausgeführt wurde oder sogar geloopt wird, bis alle Aktionen blockiert werden usw.

11

können Sie implementieren select Semantik (sowohl für die Lese- und Schreibzugriff) mit orElse (Anmerkung: es spezifisch ghc ist.) Zum Beispiel:

forever $ atomically $ 
    writeTChan chan1 "hello" `orElse` writeTChan chan2 "world" `orElse` ... 

Die Idee ist, dass, wenn eine Aktion Wiederholungen (zB Sie schreiben an chan, aber es ist voll, oder Sie lesen von chan, aber es ist leer), die zweite Aktion wird ausgeführt. Die default Anweisung ist nur eine return() als letzte Aktion in der Kette.

Hinzufügen: Wie @Dustin vermerkt, wählt go aus gutem Grund zufällige Verzweigung. Wahrscheinlich ist die einfachste Lösung, Aktionen bei jeder Iteration zu mischen, in den meisten Fällen sollte es in Ordnung sein. Die Go-Semantik korrekt zu replizieren (nur aktive Zweige mischen) ist etwas schwieriger. Wahrscheinlich ist die manuelle Überprüfung isEmptyChan für alle Zweige der Weg zu gehen.

+3

Wird das nicht Probleme mit dem Verhungern haben, wie Dustin oben bemerkte? – Dan

+3

@Dan Ich glaube, du hast Recht: (angenommen, Yuras Beispiel würde 'readTChan' verwenden)' readTChan chanN' würde nur gelesen werden, während alle chans <'N' leer sind. Also ist die Möglichkeit zu verhungern tatsächlich sogar noch schlimmer, als Sie vielleicht zuerst erwarten würden. – jberryman

+0

@Dan Sie haben Recht, ich habe eine Notiz hinzugefügt. – Yuras