2014-07-07 14 views
13

La lingua Go ha un'istruzione select che può essere utilizzata per eseguire il polling di più canali ed eseguire una determinata azione a seconda di quale canale non è vuoto per primo.Come implementare l'equivalente dell'istruzione select di Go per i canali Haskell STM?

E.g.

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

Ciò aspettare che sia chanA, chanB o chanC non è vuoto, quindi se per esempio chanB non è vuota, leggerà da chanB e memorizzare il risultato in b, quindi chiamare baz(b). Può anche essere aggiunta una clausola default:, il che significa che l'istruzione select non attenderà sui canali e invece farà qualunque sia la clausola default se tutti i canali sono vuoti.

Quale sarebbe il modo migliore per implementare qualcosa come questo per STM TChan s in Haskell? Potrebbe essere fatto ingenuamente da una catena if else: controllando se ogni chan isEmptyChan, e se non è vuoto, leggendo da esso e chiamando la funzione appropriata, oppure chiamando retry se tutti i canali sono vuoti. Mi stavo chiedendo se ci sarebbe stato un modo più elegante/idiomatico per fare questo?

Si noti che l'istruzione select di Go può includere anche istruzioni di invio nei suoi casi e completerà un'istruzione di invio solo se il canale è vuoto. Sarebbe bello se anche quella funzionalità potesse essere duplicata, anche se non sono sicuro se ci sarebbe un modo elegante per farlo.

Solo un po 'legato, ma qualcosa che ho appena notato e non sono sicuro dove post it: c'è un errore di battitura nella pagina Control.Monad.STM nella descrizione per retry:

"L'implementazione può bloccare il filo fino a quando uno dei TVars da cui ha letto è stato udpated. "

+1

Si potrebbe voler guardare a 'r ace' da 'Control.Concurrent.Async'. –

+4

Vale la pena notare che go non esegue la prima azione disponibile, ma quella disponibile, selezionata casualmente. In particolare, non affamerà i canali solo perché sono definiti più tardi o sfortunati nel percorso di selezione. – Dustin

+0

Questo è completamente diverso da quello di Go 'select'. I canali in Go sono limitati, diversamente da 'TChan' (rendendoli effettivamente utili) e' select' può essere usato con le operazioni di invio. – rightfold

risposta

5

fame evitando

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 
    ] 

Per evitare per sempre, si potrebbe invece avere un mkSelect :: [STM()] -> STM (STM()) che "nasconde" un Tvar [STM()] e lo ruota ogni Viene usato come:

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 

Estendendo tale tecnica si poteva avere una selezione che riportava se eseguiva un'azione o quale azione eseguiva o addirittura faceva un ciclo finché tutte le azioni non si bloccavano, ecc.

11

È possibile implementare select semantica (sia in lettura e scrittura) utilizzando orElse (nota: è specifico per GHC.) Ad esempio:

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

L'idea è che quando uno tentativi di azione (ad esempio, stai scrivendo a chan, ma è pieno, o stai leggendo da chan, ma è vuoto), la seconda azione viene eseguita. L'istruzione default è solo una return() come ultima azione della catena.

Aggiungi: Come indicato da @Dustin, selezionare un ramo casuale per una buona ragione. Probabilmente la soluzione più semplice è mischiare azioni su ogni iterazione, dovrebbe essere ok nella maggior parte dei casi. Replicare la semantica di go correttamente (shuffle solo i rami attivi) è un po 'più difficile. Probabilmente l'ispezione manuale di isEmptyChan per tutti i rami è la strada da percorrere.

+3

Tuttavia, non avrà problemi con la fame, come ha commentato Dustin sopra? – Dan

+3

@Dan Credo che tu abbia ragione: (supponendo che l'esempio di Yuras usasse 'readTChan')' readTChan chanN 'sarebbe sempre e solo letto da tutti mentre i chans <'N' sono vuoti. Quindi la possibilità di morire di fame è addirittura peggiore di quanto ci si potrebbe aspettare all'inizio. – jberryman

+0

@Dan Hai ragione, ho aggiunto una nota. – Yuras