Ich schreibe einen Dolmetscher für eine kleine Sprache. Diese Sprache unterstützt die Mutation, so dass ihr Evaluator für alle Variablen einen Store
verfolgt (wobei type Store = Map.Map Address Value
, type Address = Int
und data Value
ein sprachspezifischer ADT ist).Wie kann ich in verschachtelten Monaden sauber arbeiten?
Es ist auch möglich, dass Berechnungen fehlschlagen (z. B. durch Null dividieren), so dass das Ergebnis ein Either String Value
sein muss.
Die Art der mein Dolmetscher, dann ist
eval :: Environment -> Expression -> State Store (Either String Value)
wo type Environment = Map.Map Identifier Address
Spur von lokalen Bindungen hält.
Zum Beispiel eine konstante wörtliche Interpretation muss nicht in den Laden berühren, und das Ergebnis ist immer erfolgreich ist, so
eval _ (LiteralExpression v) = return $ Right v
Aber wenn wir einen binären Operator anwenden, wir brauchen den Laden zu berücksichtigen. Wenn der Benutzer beispielsweise (+ (x <- (+ x 1)) (x <- (+ x 1)))
auswertet und x
anfänglich 0
lautet, sollte das Endergebnis 3
lauten und x
sollte 2
im resultierenden Speicher sein. Dies führt zu dem Fall
eval env (BinaryOperator op l r) = do
lval <- eval env l
rval <- eval env r
return $ join $ liftM2 (applyBinop op) lval rval
Beachten Sie, dass die do
-Notation innerhalb des State Store
Monade arbeiten. Weiterhin ist die Verwendung von return
monomorph in State Store
, während die Verwendungen join
und liftM2
monomorph in der Either String
Monade sind. Das heißt, hier verwenden wir
(return . join) :: Either String (Either String Value) -> State Store (Either String Value)
und return . join
ist kein No-Op.
(Wie ersichtlich, applyBinop :: Identifier -> Value -> Value -> Either String Value
.)
Dies scheint bestenfalls verwirrend, und dies ist ein relativ einfacher Fall. Der Fall der Funktionsanwendung zum Beispiel ist wesentlich komplizierter.
Welche nützliche Best Practices sollte ich kennen, um meinen Code lesbar und schreibbar zu halten?
EDIT: Hier ist ein typisches Beispiel, das die Hässlichkeit besser darstellt. Die NewArrayC
Variante hat die Parameter length :: Expression
und element :: Expression
(es erstellt ein Array mit einer gegebenen Länge mit allen Elementen, die auf eine Konstante initialisiert sind). Ein einfaches Beispiel ist (newArray 3 "foo")
, die ["foo", "foo", "foo"]
ergibt, aber wir könnten auch schreiben (newArray (+ 1 2) (concat "fo" "oo"))
, weil wir beliebige Ausdrücke in einem NewArrayC
haben können. Aber wenn wir nennen tatsächlich
allocateMany :: Int -> Value -> State Store Address,
, die die Anzahl der Elemente nimmt zuzuweisen und den Wert für jeden Slot und gibt die Startadresse, müssen wir diese Werte entpacken. In der Logik unten sehen Sie, dass ich eine Menge Logik vervielfältige, die in die Either
Monade integriert werden sollte. Alle case
s sollte nur gebunden werden.
eval env (NewArrayC len el) = do
lenVal <- eval env len
elVal <- eval env el
case lenVal of
Right (NumV lenNum) -> case elVal of
Right val -> do
addr <- allocateMany lenNum val
return $ Right $ ArrayV addr lenNum -- result data type
left -> return left
Right _ -> return $ Left "expected number in new-array length"
left -> return left
Das ist wunderbar. Vielen Dank. – wchargin
Sie haben Recht, dass ich "das Stateful Bit äußerste" möchte (insbesondere weil die Suche nach IDs je nach Status fehlschlagen kann), aber ich verstehe nicht, wie 'außerT String (State Store) 'anstelle von' verwendet wird StateT Store (entweder String) 'erreicht dies. In der Tat scheint dies auch anders zu sein (http://stackoverflow.com/a/5076096/732016). Könnten Sie bitte erklären? – wchargin
Erweitern Sie einfach neue Typen, bis Sie etwas an der Basis bekommen. Sie werden den Unterschied schnell sehen: 'AusgenommenT String (State Store) a 'expandiert zu' Store -> (Store, Entweder String a) 'während' StateT Store (Ausgenommen String) a' expandiert zu 'Store -> Entweder String (Speichern, a) '. Was du willst, liegt natürlich bei dir. –