Как реализовать эквивалент оператора Go select для каналов Haskell STM?

Язык Go имеет оператор select, который может использоваться для опроса нескольких каналов и выполнения определенного действия в зависимости от того, какой канал сначала не пуст.

например.

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

Это будет ждать, пока тега chanA, chanB или chanC не будет пуста, тогда если, например, chanB непустое, оно будет читать из chanB и сохранить результат в b, затем вызовите baz(b). Предложение default: также может быть добавлено, что означает, что оператор select не будет ждать по каналам и вместо этого сделает то, что есть предложение default, если все каналы пустые.

Каким будет лучший способ реализовать что-то подобное для STM TChan в Haskell? Это можно было бы сделать наивно цепочкой if-else: проверка, если каждый chan isEmptyChan, и если он не пуст, а затем считывает его и вызывает соответствующую функцию или вызывает retry, если все каналы пустые. Мне было интересно, будет ли более элегантный/идиоматический способ сделать это?

Обратите внимание, что оператор Go select также может включать в себя инструкции отправки в своих случаях и будет заполнять только оператор send, если его канал пуст. Было бы здорово, если бы эта функциональность тоже дублировалась, хотя я не уверен, будет ли элегантный способ сделать это.

Только слегка связанный, но что-то, что я только что заметил, и я не уверен, где его разместить: там есть опечатка на странице Control.Monad.STM в описании для retry:

"Реализация может блокировать поток до тех пор, пока один из прочитанных ими ТВАР не будет udpated."

Ответ 1

Уклонение от голодания

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
    ]

Чтобы избежать навсегда, вместо этого вы можете использовать mkSelect :: [STM ()] -> STM (STM ()), который "скрывает" TVar [STM()] и поворачивает его, как он используется:

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

Расширяя этот метод, вы могли бы выбрать, который сообщил, выполнил ли действие или какое действие он выполнил или даже зациклил до тех пор, пока все действия не будут блокированы и т.д.

Ответ 2

Вы можете реализовать семантику select (как для чтения, так и для записи) с помощью orElse (примечание: оно специфично для ghc.) Например:

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

Идея состоит в том, что когда одно действие повторяет (например, вы пишете chan, но оно заполнено, или вы читаете chan, но оно пустое), выполняется второе действие. Оператор default представляет собой всего лишь return () как последнее действие в цепочке.

Добавить: Как отметил @Dustin, go выбирает случайную ветку по уважительной причине. Наверное, самым простым решением является перетасовка действий на каждой итерации, в большинстве случаев это должно быть хорошо. Правильная репликация семантики go (перетасовка только активных ветвей) немного сложнее. Вероятно, ручная проверка isEmptyChan для всех ветвей - это путь.