2013-10-05 5 views
9

У меня есть архитектура приложения, в которой пользовательские входы поступают в некоторые автоматы, которые запускаются в контексте потока событий и направляют пользователя в другую часть приложения. Каждая часть приложения может запускать некоторые действия на основе пользовательских входов. Однако две части приложения разделяют какое-то состояние и концептуально читают и записывают в одно и то же состояние. Предостережение состоит в том, что два «потока» не работают одновременно, один из них «приостановлен», а другой «выводит» выходы. Каков канонический способ описания этого вычисления совместного использования состояний, не прибегая к какой-либо глобальной переменной? Имеет ли смысл, чтобы два «потока» сохраняли локальные состояния, которые синхронизируются с помощью какой-либо формы передачи сообщений, даже если они не являются параллельными каким-либо образом?В функциональном реактивном программировании, как вы разделяете состояние между двумя частями приложения?

Нет образца кода, так как вопрос более концептуальный, но ответы с образцом в Haskell (с использованием любой структуры FRP) или на какой-либо другой язык приветствуются.

+1

Я думаю, что этот вопрос слишком широк, чтобы дать конкретный ответ. Любая из предложенных вами стратегий (синхронизация, FRP, глобальные вары) может быть подходящей для данной ситуации. Или, возможно, локально разделяемые 'IORef' или' MVar'. Или, если вычисления действительно находятся в одном потоке, это трансформатор монады «StateT». Мне непонятно, если «потоки» означают фактические потоки, созданные «forkIO», или если они строго концептуальны, и вы фактически используете только один поток. –

+2

@JohnL: Поскольку этот вопрос упоминает FRP, я считаю, что ответ, говорящий о том, как делиться поведением или потоком событий через приложение, будет хорошим. Я думаю, что приведение в действие поведения (или более одного, если это необходимо) через приложение является примерно хорошим выбором, но мне действительно нужно будет разобраться в деталях, прежде чем превращать это в ответ. Может быть, если никто не придет через несколько часов ... –

ответ

13

Я работаю над решением этой проблемы. Резюме высокого уровня является то, что вы:

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, но только если вы превратить вашу систему в однопоточную версию. К счастью, это возможно!

+1

Прошу прощения, но какая связь между моделью и типом «Edge»? Вся диаграмма MVC «Edge»? – chibro2

+1

Да, 'Edge' действительно следует называть' Model'. Имена все еще находятся в движении. Кроме того, да, вся диаграмма MVC - это всего лишь один большой «край». –

Смежные вопросы