Как реализовать эквивалент оператора 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
для всех ветвей - это путь.