2010-11-18 5 views
5

Ich habe einen allgemeinen Zustand, der im Wesentlichen ein 3-Tupel ist, und eine Reihe von Funktionen, die sich jeweils mit Teilen dieses Zustands befassen. Ich versuche, eine Reihe von generischen Adaptern für solche Funktionen auszuarbeiten, so dass ich sie in einer State-Monad-Pipeline verwenden kann.State monad - Funktionen anpassen, die nur mit Teilen des Staates funktionieren?

Dies ist möglicherweise völlig falsch; fühlen Sie sich frei, diesen Fall zu machen.

Ich entschuldige mich im Voraus für Mix von Java und Pidgin Scala. Ich mache das tatsächlich in Java als Lernübung, aber niemand hat Zeit, all das zu lesen. Ich habe eine Menge uninteressanter Komplexität zugunsten der Diskussion weggelassen; Mach dir keine Sorgen über die Domänenmodellierung. dies ist

Der Staat in Frage:

ImportState(row:CsvRow, contact:Contact, result:ImportResult) 

ImportResult ein von ADD, MERGE oder REJECT.

Die Funktionen I definiert haben, sind diese:

def rowToContact: ImportRow => Contact 

def findMergeCandidates: Contact => (Contact, List[Contact]) 

// merges, or declines to merge, setting the result 
def merge: (Contact, List[Contact]) => (Contact, ImportResult) 

def persist: Contact => ImportResult 

def commitOrRollback: ImportState => ImportState 

def notifyListener: ImportState => Nothing 

Die Adapter ich bisher definiert haben, sind ziemlich einfach, und befassen sich mit individuellen Eigenschaften von ImportState:

def getRow: ImportState => ImportRow 

def getContact: ImportState => Contact 

def setRow(f: _ => ImportRow): ImportState => ImportState 

def setContact(f: _ => Contact): ImportState => ImportState 

def setResult(f: _ => ImportResult): ImportState => ImportState 

Die (gebrochene) sieht in etwa so aus (in Java):

State.<ImportState>init() 
    .map(setRow(constant(row))) 
    .map(setContact(getRow.andThen(rowToContact))) 
    .map(getContact.andThen(findMergeCandidates).andThen(merge)) // this is where it falls apart 
    .map(setResult(getContact.andThen(persist))) 
    // ... lots of further processing of the persisted contact 
    .map(commitOrRollback) 
    .map(notifyListener); 

Das sofort Das Problem ist, dass merge ein Tupel (Contact, ImportResult) zurückgibt, das ich auf zwei Eigenschaften des Staates (contact und result) anwenden möchte, während die dritte Eigenschaft beibehalten wird, row.

Bisher habe ich mit ein paar Ansätze zur Anpassung der Zusammenführung kommen, die beide saugen:

  1. einige Funktionen definieren, die Tupel packen und entpacken, und sie direkt in der Pipeline verwenden. Diese Option ist extrem laut.

  2. Definieren Sie einen einmaligen Adapter für ImportState und merge. Diese Option fühlt sich an wie aufgeben.

Gibt es einen besseren Weg?

Antwort

7

Ihre Frage ist markiert Haskell - Ich hoffe, das bedeutet, dass Sie Haskell lesen können, und nicht, dass jemand 'Monaden' gesehen und hinzugefügt hat. Unter dieser Voraussetzung werde ich Haskell in dieser Antwort sprechen, da es die Sprache ist, die ich in diesen Tagen denke;)

Es gibt ein nützliches Konzept namens "funktionelle Linsen" mit ein paar Haskell Bibliothek Implementierungen.Die Kernidee ist, dass eine „Linse“ ist ein Paar von Funktionen:

data Lens a b = Lens { extract :: (a -> b), update :: (a -> b -> a) } 

Dies stellt eine funktionale Art und Weise des Erhaltens und Aktualisierung „Teile“ eine Struktur. Mit einer Art wie diese, können Sie eine Funktion schreiben, wie zum Beispiel:

subState :: Lens a b -> State a t -> State b t 
subState lens st = do 
    outer <- get 
    let (inner, result) = runState st (extract lens outer) 
    put (update lens outer inner) 
    return result 

dass Übersetzen in Java klingt wie eine interessante (und möglicherweise auch eine ziemliche Herausforderung) Übung!

+0

Danke! Ich habe es mit Haskell getaggt, weil ich Haskell mehr oder weniger lesen kann, mit Hilfe eines Haskell-Wörterbuchs. – loganj

+0

Das sieht so aus, als ob es meine Get/Set-Funktionen paaren würde. Gibt es für einen Zustand mit n Feldern ein Haskell-Idiom zum Verfassen von Extrakt- und Aktualisierungsfunktionen, so dass Sie nicht n^2-1 von jedem benötigen? – loganj

+0

@loganj: ja, im fclabels-Paket zum Beispiel ist der Linsentyp eine Instanz von "Data.Category.Category", was bedeutet, dass er ein "identity lens" definiert "id :: Lens aa" und eine "lens composition" '(.) :: Objektiv bc -> Objektiv ab -> Objektiv ac'. Die Definition ist ziemlich einfach - das neue get ist nur die Zusammensetzung der alten, und mit einer Hilfsfunktion 'modifizieren :: Lens ab -> (b -> b) -> a -> a '., Das neue' set 'ist etwas wie: 'modify lens1 outer (\ inner -> update lens2 inner x)'. Das ist ein bisschen hässlich aufgrund meiner Wahl der Argumentreihenfolge in 'update', aber hoffentlich macht es trotzdem Sinn. – mokus

0

Werfen Sie einen Blick darauf, den Tuple-Ansatz durch Fallklassen zu ersetzen. Sie erhalten viel kostenlos in einer Struktur, die so einfach zu definieren ist, insbesondere eine Compiler-generierte Kopiermethode, mit der Sie eine Kopie einer Instanz erstellen und dabei nur die Felder ändern können, die Sie ändern möchten.

2

Interessante schrieb ich diese genaue Operation letzte Nacht fclabels mit:

withGame :: (r :-> r', s :-> s') -> GameMonad r' s' a -> GameMonad r s a 
withGame (l1,l2) act = do 
    (r,s) <- (,) <$> askM l1 <*> getM l2 
    (a, s') <- liftIO $ runGame r s act 
    setM l2 s' 
    return a 

GameMonad ein neuer Typ ist, der eine Monade Transformator Stapel Zustand, Leser, IO. Ich benutze auch ein bisschen anwendungsorientierten Funktor-Code, lass dich nicht davon abbringen, es ist so ziemlich wie Mokus.