Я работаю над решением этой проблемы. Резюме высокого уровня является то, что вы:
A) перегоняете весь свой параллельный код в чистую и однопоточную спецификацию
B) однопоточная спецификация использует StateT
для обмена общего государства
общая архитектура вдохновлена модель-view-контроллером. У Вас есть:
- контроллеры, которые effectful входы
- Представления, которые effectful выходы
- модель, которая представляет собой чистый поток трансформации
Модель может взаимодействовать только с одним контроллером и один вид. Однако оба контроллера и представления являются моноидами, поэтому вы можете объединить несколько контроллеров в один контроллер и несколько видов в один вид. Схематически это выглядит следующим образом:
controller1 - -> view1
\ /
controller2 ---> controllerTotal -> model -> viewTotal---> view2
/ \
controller3 - -> view3
\______ ______/ \__ __/ \___ ___/
v v v
Effectful Pure Effectful
Модель представляет собой чистый, однопоточный поток трансформатора, который реализует Arrow
и ArrowChoice
. Причина в том, что:
Arrow
является однопоточных эквивалентно параллельности
ArrowChoice
является однопоточных эквивалентно параллельности
В этом случае я использую нажим на основе pipes
, которые, как представляется, имеют правильный пример Arrow
и ArrowChoice
, хотя я все еще работаю над проверкой законов, поэтому это решение все еще экспериментально, пока я не завершу свои доказательства. Для тех, кто интересуется, соответствующие типы и примеры:
newtype Edge m r a b = Edge { unEdge :: a -> Pipe a b m r }
instance (Monad m) => Category (Edge m r) where
id = Edge push
(Edge p2) . (Edge p1) = Edge (p1 >~> p2)
instance (Monad m) => Arrow (Edge m r) where
arr f = Edge (push />/ respond . f)
first (Edge p) = Edge $ \(b, d) ->
evalStateP d $ (up \>\ unsafeHoist lift . p />/ dn) b
where
up() = do
(b, d) <- request()
lift $ put d
return b
dn c = do
d <- lift get
respond (c, d)
instance (Monad m) => ArrowChoice (Edge m r) where
left (Edge k) = Edge (bef >=> (up \>\ (k />/ dn)))
where
bef x = case x of
Left b -> return b
Right d -> do
_ <- respond (Right d)
x2 <- request()
bef x2
up() = do
x <- request()
bef x
dn c = respond (Left c)
Модель также должна быть монада-трансформатором. Причина в том, что мы хотим встроить StateT
в базовую монаду, чтобы отслеживать общее состояние. В этом случае pipes
подходит к счету.
Последний фрагмент головоломки представляет собой сложный реальный пример принятия сложной параллельной системы и перегонки ее в чистый однопоточный эквивалент. Для этого я использую мою предстоящую библиотеку rcpl
(сокращение от «read-concurrent-print-loop»). Цель библиотеки rcpl
- предоставить параллельный интерфейс консоли, который позволяет читать входные данные от пользователя при одновременной печати на консоли, но без вывода на печать ввода пользователя. Репозиторий Github для него здесь:
Link to Github Repository
Моей первой реализации этой библиотеки была широко распространенная параллелизм и передачи сообщений, но страдает от нескольких параллельности ошибок, которые я не мог решить. Затем, когда я придумал mvc
(кодовое имя для моей FRP-подобной структуры, сокращение от «model-view-controller»), я подумал, что rcpl
будет отличным тестовым примером, чтобы узнать, готов ли mvc
к прайм-тайму.
Я взял всю логику rcpl
и превратил ее в единую чистую трубу. Это то, что вы найдете в this module, а полная логика целиком находится в пределах rcplCore
pipe.
Это аккуратно, потому что теперь, когда реализация чиста, я могу проверить ее и проверить определенные свойства! Например, одно свойства я мог бы QuickCheck, что существует ровно одна команды терминала за ключом пользователя нажатия x
ключа, который я хотел бы указать, как это:
>>> quickCheck $ \n -> length ((`evalState` initialStatus) $ P.toListM $ each (replicate n (Key 'x')) >-> runEdge (rcplCore t)) == n || n < 0
n
этого количество раз, что я давлю ключ x
. Выполнение этого теста дает следующий результат:
*** Failed! Falsifiable (after 17 tests and 6 shrinks):
78
QuickCheck обнаружил, что моя собственность была ложной! Более того, поскольку код является ссылочно прозрачным, QuickCheck может сузить контрпример до нарушения минимального воспроизведения. После нажатия 78 клавиш драйвер терминала испускает новую строку, потому что консоль имеет ширину 80 символов, и в этом случае запрос приглашает два символа ("> "
). Это тот тип собственности, с которым мне было бы очень сложно проверить, была ли параллелизм и IO
заражены всей моей системой.
Наличие чистой установки отлично по другой причине: все полностью воспроизводимо! Если я храню журнал всех входящих событий, то в любой момент, когда появляется ошибка, я могу воспроизвести события и отлично воспроизвести тестовый пример, который я могу добавить в свой тестовый набор.
Однако, самое важное преимущество чистоты - это способность более легко рассуждать о коде, как неофициально, так и формально. Когда вы удаляете планировщик Haskell из уравнения, вы можете доказать статичность своего кода, который вы не смогли бы доказать, когда вам нужно зависеть от параллельной среды выполнения с неформальной семантикой. Это действительно оказалось действительно полезным даже для неофициальных рассуждений, потому что, когда я преобразовал свой код для использования mvc
, он все еще имел несколько ошибок, но их было намного легче отлаживать и удалять, чем упрямые ошибки параллелизма с моей первой итерации.
rcpl
пример использует StateT
разделить глобальное состояние между различными компонентами, так многословно ответ на ваш вопрос: Вы можете использовать StateT
, но только если вы превратить вашу систему в однопоточную версию. К счастью, это возможно!
Я думаю, что этот вопрос слишком широк, чтобы дать конкретный ответ. Любая из предложенных вами стратегий (синхронизация, FRP, глобальные вары) может быть подходящей для данной ситуации. Или, возможно, локально разделяемые 'IORef' или' MVar'. Или, если вычисления действительно находятся в одном потоке, это трансформатор монады «StateT». Мне непонятно, если «потоки» означают фактические потоки, созданные «forkIO», или если они строго концептуальны, и вы фактически используете только один поток. –
@JohnL: Поскольку этот вопрос упоминает FRP, я считаю, что ответ, говорящий о том, как делиться поведением или потоком событий через приложение, будет хорошим. Я думаю, что приведение в действие поведения (или более одного, если это необходимо) через приложение является примерно хорошим выбором, но мне действительно нужно будет разобраться в деталях, прежде чем превращать это в ответ. Может быть, если никто не придет через несколько часов ... –