2012-12-17 10 views
14

Sto scrivendo un programma che viene eseguito come un demone. Per creare il demone, l'utente fornisce un insieme di implementazioni per ciascuna delle classi richieste (uno di loro è un database) Tutte queste classi hanno funzioni hanno tipo firme del modulo StateT s IO a, ma s è diverso per ogni classeCombinazione di più stati nello Stato

Supponiamo ciascuna delle classi segue questo schema:

import Control.Monad (liftM) 
import Control.Monad.State (StateT(..), get) 

class Hammer h where 
    driveNail :: StateT h IO() 

data ClawHammer = MkClawHammer Int -- the real implementation is more complex 

instance Hammer ClawHammer where 
    driveNail = return() -- the real implementation is more complex 

-- Plus additional classes for wrenches, screwdrivers, etc. 

Ora può definire un record che rappresenta l'implementazione scelto dal dall'utente per ogni "slot".

data MultiTool h = MultiTool { 
    hammer :: h 
    -- Plus additional fields for wrenches, screwdrivers, etc. 
    } 

E il demone fa la maggior parte del suo lavoro nella monade StateT (MultiTool h ...) IO() .

Ora, dal momento che il multiutensile contiene un martello, posso usarlo in qualsiasi situazione in cui è necessario un martello. In altre parole, il MultiTool tipo può implementare qualsiasi delle classi in esso contenuti, se scrivo codice come questo:

stateMap :: Monad m => (s -> t) -> (t -> s) -> StateT s m a -> StateT t m a 
stateMap f g (StateT h) = StateT $ liftM (fmap f) . h . g 

withHammer :: StateT h IO() -> StateT (MultiTool h) IO() 
withHammer runProgram = do 
    t <- get 
    stateMap (\h -> t {hammer=h}) hammer runProgram 

instance Hammer h => Hammer (MultiTool h) where 
    driveNail = withHammer driveNail 

Ma le implementazioni di withHammer, withWrench, withScrewdriver, ecc sono sostanzialmente identiche. Sarebbe bello essere in grado di scrivere qualcosa di simile ...

--withMember accessor runProgram = do 
-- u <- get 
-- stateMap (\h -> u {accessor=h}) accessor runProgram 

-- instance Hammer h => Hammer (MultiTool h) where 
-- driveNail = withMember hammer driveNail 

Ma, naturalmente, che non verrà compilato.

Sospetto che la mia soluzione sia troppo orientata agli oggetti. C'è un modo migliore? Trasformatori Monad, forse? Grazie in anticipo per eventuali suggerimenti.

+0

Per inciso, ho fatto una rapida modifica al codice, perché nella vostra semplificazione omettendo l'attuazione del 'ClawHammer' hai prodotto qualcosa che probabilmente non era quello che volevi dire. –

risposta

24

Se vuoi andare con un grande stato globale come nel tuo caso, allora quello che vuoi usare è l'obiettivo, come suggerito da Ben. Raccomando anche la libreria della lente di Edward Kmett. Tuttavia, c'è un altro, forse più bello.

I server hanno la proprietà che il programma esegue continuamente ed esegue la stessa operazione su uno spazio di stato.Il problema inizia quando si desidera modularizzare il server, nel qual caso si desidera più di un semplice stato globale. Vuoi che i moduli abbiano il loro stato.

Pensiamo di un modulo come qualcosa che trasforma un richiesta ad un risposta:

Module :: (Request -> m Response) -> Module m 

Ora se ha qualche stato, allora questo stato diventa notevole nel fatto che il modulo potrebbe dare una diversa rispondi la prossima volta. Ci sono un certo numero di modi per farlo, ad esempio, il seguente:

Module :: s -> ((Request, s) -> m (Response s)) -> Module m 

Ma un modo molto più bello e equivalente per esprimere questo è il seguente costruttore (costruiremo un tipo attorno ad esso presto):

Module :: (Request -> m (Response, Module m)) -> Module m 

Questo modulo mappa una richiesta di risposta, ma lungo il percorso restituisce anche una nuova versione di se stesso. Andiamo oltre e fare richieste e le risposte polimorfa:

Module :: (a -> m (b, Module m a b)) -> Module m a b 

Ora, se il tipo di uscita di un modulo corrisponde il tipo di ingresso di un altro modulo, allora si può comporre come funzioni regolari. Questa composizione è associativa e ha un'identità polimorfica. Questo suona molto come una categoria, e infatti lo è! È una categoria, un funtore applicativo e una freccia.

newtype Module m a b = 
    Module (a -> m (b, Module m a b)) 

instance (Monad m) => Applicative (Module m a) 
instance (Monad m) => Arrow (Module m) 
instance (Monad m) => Category (Module m) 
instance (Monad m) => Functor (Module m a) 

Possiamo ora comporre due moduli che hanno il loro stato locale individuale senza nemmeno saperlo! Ma non è sufficiente. Vogliamo di più. Che ne dici di moduli che possono essere cambiati? Estendiamo il nostro sistema di modulo piccolo in modo tale che i moduli possono effettivamente scegliere non di dare una risposta:

newtype Module m a b = 
    Module (a -> m (Maybe b, Module m a b)) 

Questo permette di un'altra forma di composizione che è ortogonale a (.): Ora il nostro tipo è anche una famiglia di Alternative funtori:

instance (Monad m) => Alternative (Module m a) 

Ora un modulo può scegliere se rispondere a una richiesta e, in caso contrario, verrà provato il modulo successivo. Semplice. Hai appena reinventato la categoria filo. =)

Naturalmente non è necessario reinventarlo. La libreria Netwire implementa questo modello di progettazione e viene fornito con una vasta libreria di "moduli" predefiniti (denominati fili). Vedi il modulo Control.Wire per un tutorial.

+5

Risposta incredibilmente eccellente! – AndrewC

6

Questo suona molto simile a un'applicazione di obiettivi.

Le lenti sono una specifica di un sottocampo di alcuni dati. L'idea è che tu abbia qualche valore toolLens e funzioni view e set in modo che view toolLens :: MultiTool h -> h recuperi lo strumento e set toolLens :: MultiTool h -> h -> MultiTool h lo sostituisca con un nuovo valore. Quindi puoi facilmente definire il tuo withMember come una funzione semplicemente accettando un obiettivo.

La tecnologia degli obiettivi ha fatto grandi progressi di recente e ora sono incredibilmente capaci. La libreria più potente in circolazione al momento della scrittura è la libreria lens di Edward Kmett, che è un po 'troppo da ingoiare, ma piuttosto semplice una volta trovate le funzionalità che si desidera. Puoi anche cercare altre domande sugli obiettivi qui su SO, ad es. Functional lenses che collega a lenses, fclabels, data-accessor - which library for structure access and mutation is better o al tag lenses.

14

Ecco un esempio concreto di come utilizzare lens come tutti gli altri stanno parlando. Nell'esempio di codice seguente, Type1 è lo stato locale (ad es. Il tuo martello) e Type2 è lo stato globale (ad esempio il tuo multitool). lens fornisce la funzione zoom che consente di eseguire un calcolo stato localizzato che ingrandisce su qualsiasi campo definito da una lente:

import Control.Lens 
import Control.Monad.Trans.Class (lift) 
import Control.Monad.Trans.State 

data Type1 = Type1 { 
    _field1 :: Int , 
    _field2 :: Double} 

field1 :: SimpleLens Type1 Int 
field1 = lens _field1 (\x a -> x { _field1 = a}) 

field2 :: SimpleLens Type1 Double 
field2 = lens _field2 (\x a -> x { _field2 = a}) 

data Type2 = Type2 { 
    _type1 :: Type1 , 
    _field3 :: String} 

type1 :: SimpleLens Type2 Type1 
type1 = lens _type1 (\x a -> x { _type1 = a}) 

field3 :: SimpleLens Type2 String 
field3 = lens _field3 (\x a -> x { _field3 = a}) 

localCode :: StateT Type1 IO() 
localCode = do 
    field1 += 3 
    field2 .= 5.0 
    lift $ putStrLn "Done!" 

globalCode :: StateT Type2 IO() 
globalCode = do 
    f1 <- zoom type1 $ do 
     localCode 
     use field1 
    field3 %= (++ show f1) 
    f3 <- use field3 
    lift $ putStrLn f3 

main = runStateT globalCode (Type2 (Type1 9 4.0) "Hello: ") 

zoom non è limitato a immediati sotto-campi di un tipo.Dal momento che le lenti sono componibili, è possibile ingrandire in profondità come si desidera in una sola operazione solo facendo qualcosa di simile:

zoom (field1a . field2c . field3b . field4j) $ do ... 
+0

L'ultimo svantaggio di questo approccio è che 'Type1' è direttamente annidato all'interno di' Tipo2' e la piena conoscenza di quel tipo è richiesta. Ciò rende l'astrazione che perde IMHO. –

Problemi correlati