2013-10-05 7 views
9

Ho una certa architettura di applicazione in cui gli input dell'utente fluiscono su alcuni automi, che vengono eseguiti nel contesto del flusso di eventi e indirizzano l'utente a diverse parti dell'applicazione. Ogni parte dell'applicazione può eseguire alcune azioni in base agli input dell'utente. Tuttavia, due parti dell'applicazione condividono alcuni stati e sono concettualmente in grado di leggere e scrivere nello stesso stato. L'avvertenza è che i due "thread" non funzionano contemporaneamente, uno di questi è "messo in pausa" mentre l'altro "produce" output. Qual è il modo canonico per descrivere questo computazione dello stato, senza ricorrere a qualche variabile globale? Ha senso che i due "thread" mantengano gli stati locali sincronizzati da una qualche forma di passaggio di messaggi, anche se non sono concomitanti in alcun modo?Nella programmazione reattiva funzionale, come si condivide lo stato tra due parti dell'applicazione?

Non c'è un esempio di codice poiché la domanda è più concettuale, ma le risposte con l'esempio in Haskell (utilizzando qualsiasi framework FRP) o qualche altra lingua sono benvenute.

+1

Penso che questa domanda sia troppo ampia per dare una risposta specifica. Qualunque delle strategie suggerite (sincronizzazione, FRP, vars globali) può essere appropriata per la situazione data. O forse un 'IORef' localmente condiviso o' MVar'. O se i calcoli sono realmente in un singolo thread, un trasformatore monade 'StateT'. Non mi è chiaro se "" threads "" significa thread reali creati da 'forkIO', o se sono strettamente concettuali e in realtà stai facendo girare solo un thread. –

+2

@JohnL: Poiché questa domanda parla di FRP, immagino che una risposta che parli di come condividere un comportamento o un flusso di eventi attraverso un'applicazione sarebbe buona. Penso che il threading di un comportamento (o più di uno, se appropriato) attraverso l'applicazione sia approssimativamente una buona scelta, ma dovrei davvero elaborare i dettagli prima di trasformarlo in una risposta. Forse se nessuno arriva in poche ore ... –

risposta

13

Ho lavorato a una soluzione a questo problema. La sintesi di alto livello è che si:

A) distillare tutto il codice concorrente in una specifica puro e single-threaded

B) La specifica single-threaded utilizza StateT condividere Stato comune

Il l'architettura generale è ispirata al model-view-controller. Si ha:

  • controllori, che sono ingressi effectful
  • Vista, che sono uscite effectful
  • Un modello, che è una corrente pura trasformazione

Il modello può interagire solo con un controllore e una vista. Tuttavia, sia i controller che le viste sono monoidi, quindi è possibile combinare più controller in un singolo controller e più viste in un'unica vista. Schematicamente, sembra che questo:

controller1 -           -> view1 
       \          /
controller2 ---> controllerTotal -> model -> viewTotal---> view2 
      /          \ 
controller3 -           -> view3 

        \______ ______/ \__ __/ \___ ___/ 
         v    v   v 
        Effectful  Pure Effectful 

Il modello è una pura, trasformatore thread singolo flusso che implementa Arrow e ArrowChoice. Il motivo è che:

  • Arrow è l'equivalente singolo thread di parallelismo
  • ArrowChoice è l'equivalente singolo thread di concorrenza

In questo caso utilizzo push-base pipes, che sembrano avere un'istanza corretta Arrow e ArrowChoice, anche se sto ancora lavorando per verificare le leggi, quindi questa soluzione è ancora sperimentale fino a quando non ho completato le loro prove. Per coloro che sono curiosi, il tipo e casi rilevanti sono:

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) 

Il modello deve essere anche un trasformatore monade. Il motivo per cui è che vogliamo incorporare StateT nella monade di base per tenere traccia dello stato condiviso. In questo caso, pipes si adatta alla fattura.

L'ultimo pezzo del puzzle è un sofisticato esempio del mondo reale di prendere un complesso sistema concorrente e distillarlo in un equivalente a thread singolo puro. Per questo uso la mia prossima libreria rcpl (abbreviazione di "read-concurrent-print-loop"). Lo scopo della libreria rcpl è di fornire un'interfaccia concorrente alla console che consente di leggere l'input dall'utente mentre si stampa contemporaneamente sulla console, ma senza l'output stampato che blocca l'input dell'utente. Il repository Github perché è qui:

Link to Github Repository

mia implementazione originale di questa biblioteca ha avuto concorrenza pervasivo e scambio di messaggi, ma è stato afflitto da diversi bug di concorrenza che non ho potuto risolvere. Poi, quando sono arrivato con mvc (il nome in codice per il mio framework simile a FRP, abbreviazione di "model-view-controller"), ho pensato che rcpl sarebbe stato un ottimo banco di prova per vedere se mvc fosse pronto per la prima serata.

Ho preso l'intera logica del rcpl e l'ho trasformato in un unico tubo puro. Questo è quello che troverai in this module e la logica totale è contenuta interamente all'interno dello rcplCore pipe.

Questo è pulito, perché ora che l'implementazione è pura, posso controllarla rapidamente e verificare determinate proprietà! Ad esempio, una proprietà potrei voler QuickCheck è che c'è esattamente un comando da terminale per utente premere il tasto della chiave x, che vorrei specificare come questo:

>>> quickCheck $ \n -> length ((`evalState` initialStatus) $ P.toListM $ each (replicate n (Key 'x')) >-> runEdge (rcplCore t)) == n || n < 0 

n è il numero di volte che premo la chiave x. L'esecuzione di tale test produce il seguente risultato:

*** Failed! Falsifiable (after 17 tests and 6 shrinks): 
78 

QuickCheck ha scoperto che la mia proprietà era falsa! Inoltre, poiché il codice è referenzialmente trasparente, QuickCheck può restringere il controesempio alla violazione della riproduzione minima. Dopo 78 tasti premuti, il driver del terminale emette una nuova riga perché la console ha una larghezza di 80 caratteri e due caratteri sono occupati dal prompt ("> " in questo caso). Questo è il tipo di proprietà che sarebbe molto difficile verificare se la concorrenza e IO infettano il mio intero sistema.

Avere una configurazione pura è ottimo per un altro motivo: tutto è completamente riproducibile! Se memorizzo un log di tutti gli eventi in arrivo, ogni volta che si verifica un errore riesco a riprodurre gli eventi e ho un caso di test perfettamente riproducibile che posso aggiungere alla mia suite di test.

Tuttavia, il vantaggio più importante della purezza è la possibilità di ragionare più facilmente sul codice, sia in modo informale che formale. Quando rimuovi l'utilità di pianificazione di Haskell dall'equazione, puoi provare le cose in modo statico sul tuo codice che non potresti provare quando devi fare affidamento su un runtime simultaneo con una semantica informalmente specificata. Ciò si è rivelato davvero utile anche per il ragionamento informale, perché quando ho trasformato il mio codice per utilizzare mvc c'erano ancora diversi bug, ma questi erano molto più facili da debugare e rimuovere rispetto ai bug di concorrenza testardi della mia prima iterazione.

L'rcpl esempio utilizza StateT per condividere lo stato globale tra diversi componenti, quindi la risposta prolisso alla tua domanda è: è possibile utilizzare StateT, ma solo se si trasformare il vostro sistema a una versione single-threaded. Fortunatamente è possibile!

+1

Mi dispiace ma qual è la connessione tra il modello e il tipo "Edge"? L'intero diagramma MVC è un 'Edge'? – chibro2

+1

Sì, 'Edge' dovrebbe essere chiamato' Modello'. I nomi sono ancora in divenire. Inoltre, sì, l'intero diagramma MVC è solo un grande 'Edge'. –

Problemi correlati