2012-12-17 9 views
14

Ich schreibe ein Programm, das als Daemon läuft. den Daemon zu erstellen, liefert der Benutzer eine Reihe von Implementierungen für jede der erforderlichen Klassen (einer von ihnen ist eine Datenbank) Alle diese Klassen haben Funktionen Typsignaturen der Form StateT s IO a, aber s anders haben, ist für jede Klasse.Kombinieren mehrerer Zustände in StateT

Angenommen, jede der Klassen dieses Muster folgt:

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. 

Jetzt kann ich definieren einen Datensatz, der die Umsetzung von der Benutzer für jeden „Schlitz“ gewählt darstellt.

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

Und der Dämon hat den größten Teil seiner Arbeit in der StateT (MultiTool h ...) IO() Monade.

Jetzt, da das Multitool einen Hammer enthält, kann ich es in jeder Situation verwenden, wo ein Hammer benötigt wird. Mit anderen Worten, die MultiTool Typ können jede der Klassen implementieren es enthält, wenn ich Code wie folgt schreiben:

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 

Aber die Implementierungen von withHammer, withWrench, withScrewdriver usw. sind im Grunde identisch. Es wäre schön, um etwas so schreiben ...

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

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

Aber das ist natürlich nicht kompiliert werden.

Ich vermute, dass meine Lösung zu objektorientiert ist. Gibt es einen besseren Weg? Monade Transformatoren, vielleicht? Vielen Dank im Voraus für Anregungen.

+0

, machte ich schnell bearbeiten, weil in der Vereinfachung, um Ihren Code die Implementierung von 'ClawHammer' Weglassen du hast etwas produziert, das wahrscheinlich nicht das war, was du meintest. –

Antwort

24

Wenn Sie mit einem großen globalen Staat wie in Ihrem Fall gehen wollen, dann sind Linsen, wie von Ben vorgeschlagen. Ich empfehle auch Edward Kmetts Objektiv Bibliothek. Aber es gibt noch einen anderen, vielleicht schöneren Weg.

Server haben die Eigenschaft, dass das Programm kontinuierlich ausgeführt wird und dieselbe Operation über einen Statusbereich ausführt.Der Fehler beginnt, wenn Sie Ihren Server modularisieren möchten. In diesem Fall möchten Sie mehr als nur einen globalen Status. Sie möchten, dass Module ihren eigenen Status haben.

Lassen Sie uns eines Moduls als etwas denken, das eine Antrag auf eine Antwort verwandelt:

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

Nun, wenn es einige Staat hat, dann ist dieser Zustand, dass noticable wird das Modul möglicherweise einen anderen geben antworte das nächste Mal. Es gibt eine Reihe von Möglichkeiten, dies zu tun, zum Beispiel folgende:

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

Aber eine viel schöner und gleichwertige Möglichkeit, dies auszudrücken, ist der folgende Konstruktor (wir einen Typen um es bald bauen):

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

Dieses Modul ordnet eine Anfrage einer Antwort zu, gibt aber auch eine neue Version von sich selbst zurück. Gehen wir weiter und stellen Anfragen und Antworten polymorphen:

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

Nun, wenn der Ausgabetyp eines Moduls Eingangstyp eines anderen Moduls übereinstimmt, dann können Sie sie wie normale Funktionen zusammenstellen. Diese Zusammensetzung ist assoziativ und hat eine polymorphe Identität. Das hört sich nach einer Kategorie an, und tatsächlich ist es das! Es ist eine Kategorie, ein Anwendungsfunktor und ein Pfeil.

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) 

Wir können nun zwei Module zusammenstellen, die ihren eigenen individuellen lokalen Zustand haben, ohne auch nur darüber zu wissen! Aber das ist nicht genug. Wir wollen mehr. Wie wäre es mit Modulen, die zwischengeschaltet werden können? Lassen Sie uns unser kleines Modulsystem erstrecken, so dass die Module tatsächlich keine Antwort geben wählen:

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

Diese andere Form der Zusammensetzung ermöglicht, die (.) orthogonal ist: Jetzt ist unsere Art ist auch eine Familie von Alternative functors:

Jetzt kann ein Modul wählen, ob auf eine Anfrage reagiert werden soll, und wenn nicht, wird das nächste Modul ausprobiert. Einfach. Sie haben gerade die Drahtkategorie neu erfunden. =)

Natürlich müssen Sie das nicht neu erfinden. Die Bibliothek Netwire implementiert dieses Entwurfsmuster und wird mit einer großen Bibliothek vordefinierter "Module" (Drähte genannt) geliefert. Ein Tutorial finden Sie im Modul Control.Wire.

+5

Unverkennbar ausgezeichnete Antwort! – AndrewC

6

Dies klingt sehr nach einer Anwendung von Linsen.

Linsen sind eine Spezifikation eines Unterfeldes einiger Daten. Die Idee ist, Sie haben einen Wert toolLens und Funktionen view und set, so dass view toolLens :: MultiTool h -> h holt das Werkzeug und set toolLens :: MultiTool h -> h -> MultiTool h ersetzt es mit einem neuen Wert. Dann können Sie einfach Ihre withMember als Funktion definieren, die gerade ein Objektiv annimmt.

Die Lens-Technologie hat in letzter Zeit sehr viel Fortschritte gemacht, und sie sind jetzt unglaublich fähig. Die mächtigste Bibliothek zum Zeitpunkt des Schreibens ist Edward Kmetts lens-Bibliothek, die ein bisschen viel zu schlucken ist, aber ziemlich einfach, sobald Sie die Funktionen finden, die Sie wollen. Sie können auch hier auf SO nach mehr Fragen über Linsen suchen, z.B. Functional lenses, die auf lenses, fclabels, data-accessor - which library for structure access and mutation is better oder das lenses-Tag verweist.

14

Hier ist ein konkretes Beispiel, wie man lens verwendet, wie jeder andere auch spricht. Im folgenden Codebeispiel ist Type1 der lokale Status (d. H. Ihr Hammer) und Type2 ist der globale Status (d. H. Ihr Multitool). lens bietet die Funktion, die zoom Sie eine lokalisierte Zustandsberechnung ermöglicht die Ausführung, die durch eine Linse auf ein beliebiges Feld definiert zoomt:

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 ist nicht auf unmittelbare Subfelder eines Typs beschränkt.Da Linsen zusammensetzbare sind, können Sie so tief vergrößern, wie Sie nur in einem einzigen Vorgang wollen durch so etwas wie tun: Übrigens

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

Der ultimative Nachteil dieses Ansatzes besteht darin, dass "Type1" direkt in "Type2" verschachtelt ist und vollständige Kenntnis dieses Typs erforderlich ist. Das macht die Abstraktion undicht IMHO. –

Verwandte Themen