Ich versuche, in Haskell einen typsicheren Frage-Antwort-Fluss zu erstellen. Ich modelliere QnA als gerichteten Graph, ähnlich einem FSM.Typsicherer Fluss (Zustandsmaschine)
Jeder Knoten in dem Graphen stellt eine Frage:
data Node s a s' = Node {
question :: Question a,
process :: s -> a -> s'
}
s
ist der Eingangszustand, a
ist die Antwort auf die Frage und s'
ist der Ausgangszustand. Knoten hängen vom Eingangszustand s
ab, was bedeutet, dass wir für die Verarbeitung der Antwort in einem bestimmten Zustand vorher sein müssen.
Question a
stellen eine einfache Frage/Antwort dar, die eine Antwort vom Typ a
erzeugt.
Durch typsichere I bedeuten, zum Beispiel einen Knoten Node2 :: si -> a -> s2
gegeben, wenn auf si
s1
hängt dann alle endende Pfade mit Node2
müssen durch einen Knoten sein, die Weitergabe s1
ersten produziert. (Wenn s1 == si
, dann müssen alle Vorgänger von Node2
s1
produzieren).
Ein Beispiel
QnA: In einer Online-Shopping-Website, müssen wir den Körper des Benutzers Größe und Lieblingsfarbe fragen.
e1
: fragen Sie den Benutzer, ob er seine Größe kennt. Wenn ja, gehen Sie zue2
, gehen Sie andernfalls zue3
e2
: fragen Sie die Größe des Benutzers und gehen Sie zuef
, um die Farbe zu fragen.e3
: (Benutzer kennt ihre Größe nicht), fragen Sie das Gewicht des Benutzers und gehen Sie zue4
.e4
: (nache3
) fragt Höhe des Benutzers und ihre Größe berechnen und gehen Sie zuef.
ef
: Benutzer der Lieblingsfarbe stellen und den Fluss mit demFinal
Ergebnis beenden.
In meinem Modell, schließen Edge
s Node
s zueinander:
data Edge s sf where
Edge :: EdgeId -> Node s a s' -> (s' -> a -> Edge s' sf) -> Edge s sf
Final :: EdgeId -> Node s a s' -> (s' -> a -> sf) -> Edge s sf
sf
ist das Endergebnis der QnA, das hier ist: (Bool, Size, Color)
.
Der QnA-Status zu jedem Zeitpunkt kann durch ein Tupel dargestellt werden: (s, EdgeId)
. Dieser Zustand ist serialisierbar und wir sollten in der Lage sein, einen QnA fortzusetzen, indem wir nur diesen Zustand kennen.
saveState :: (Show s) => (s, Edge s sf) -> String
saveState (s, Edge eid n _) = show (s, eid)
getEdge :: EdgeId -> Edge s sf
getEdge = undefined --TODO
respond :: s -> Edge s sf -> Input -> Either sf (s', Edge s' sf)
respond s (Edge ...) input = Right (s', Edge ...)
respond s (Final ...) input = Left s' -- Final state
-- state = serialized (s, EdgeId)
-- input = user's answer to the current question
main' :: String -> Input -> Either sf (s', Edge s' sf)
main' state input =
let (s, eid) = read state :: ((), EdgeId) --TODO
edge = getEdge eid
in respond s input edge
Voll Code:
{-# LANGUAGE GADTs, RankNTypes, TupleSections #-}
type Input = String
type Prompt = String
type Color = String
type Size = Int
type Weight = Int
type Height = Int
data Question a = Question {
prompt :: Prompt,
answer :: Input -> a
}
-- some questions
doYouKnowYourSizeQ :: Question Bool
doYouKnowYourSizeQ = Question "Do you know your size?" read
whatIsYourSizeQ :: Question Size
whatIsYourSizeQ = Question "What is your size?" read
whatIsYourWeightQ :: Question Weight
whatIsYourWeightQ = Question "What is your weight?" read
whatIsYourHeightQ :: Question Height
whatIsYourHeightQ = Question "What is your height?" read
whatIsYourFavColorQ :: Question Color
whatIsYourFavColorQ = Question "What is your fav color?" id
-- Node and Edge
data Node s a s' = Node {
question :: Question a,
process :: s -> a -> s'
}
data Edge s sf where
Edge :: EdgeId -> Node s a s' -> (s' -> a -> Edge s' sf) -> Edge s sf
Final :: EdgeId -> Node s a s' -> (s' -> a -> sf) -> Edge s sf
data EdgeId = E1 | E2 | E3 | E4 | Ef deriving (Read, Show)
-- nodes
n1 :: Node() Bool Bool
n1 = Node doYouKnowYourSizeQ (const id)
n2 :: Node Bool Size (Bool, Size)
n2 = Node whatIsYourSizeQ (,)
n3 :: Node Bool Weight (Bool, Weight)
n3 = Node whatIsYourWeightQ (,)
n4 :: Node (Bool, Weight) Height (Bool, Size)
n4 = Node whatIsYourHeightQ (\ (b, w) h -> (b, w * h))
n5 :: Node (Bool, Size) Color (Bool, Size, Color)
n5 = Node whatIsYourFavColorQ (\ (b, i) c -> (b, i, c))
-- type-safe edges
e1 = Edge E1 n1 (const $ \ b -> if b then e2 else e3)
e2 = Edge E2 n2 (const $ const ef)
e3 = Edge E3 n3 (const $ const e4)
e4 = Edge E4 n4 (const $ const ef)
ef = Final Ef n5 const
ask :: Edge s sf -> Prompt
ask (Edge _ n _) = prompt $ question n
ask (Final _ n _) = prompt $ question n
respond :: s -> Edge s sf -> Input -> Either sf (s', Edge s' sf)
respond s (Edge _ n f) i =
let a = (answer $ question n) i
s' = process n s a
n' = f s' a
in Right undefined --TODO n'
respond s (Final _ n f) i =
let a = (answer $ question n) i
s' = process n s a
in Left undefined --TODO s'
-- User Interaction:
saveState :: (Show s) => (s, Edge s sf) -> String
saveState (s, Edge eid n _) = show (s, eid)
getEdge :: EdgeId -> Edge s sf
getEdge = undefined --TODO
-- state = serialized (s, EdgeId) (where getEdge :: EdgeId -> Edge s sf)
-- input = user's answer to the current question
main' :: String -> Input -> Either sf (s', Edge s' sf)
main' state input =
let (s, eid) = undefined -- read state --TODO
edge = getEdge eid
in respond s edge input
Es ist wichtig für mich, die Kanten typsicher zu halten.Bedeutung zum Beispiel falsch Verknüpfung e2
zu e3
muss ein Typ Fehler sein: e2 = Edge E2 n2 (const $ const ef)
ist in Ordnung durch e2 = Edge E2 n2 (const $ const e3)
muss ein Fehler sein.
Ich habe meine Fragen mit --TOOD
angegeben:
meine Kriterien Gegeben für Kanten typsicher, halten
Edge s sf
muss einen Eingabetyp Variable (s
) haben wie kann ich danngetEdge :: EdgeId -> Edge s sf
Funktion erstellen?Wie kann ich die
respond
Funktion, die den aktuellen Zustands
und aktuellen RandEdge s sf
entweder kehrt den Endzustand (wenn die aktuellen Rand istFinal
) oder den nächsten Zustand und die nächste Flanke(s', Edge s' sf)
gegeben schaffen?
Mein Design von Node s a s'
und Edge s sf
könnte einfach falsch. Ich muss nicht dabei bleiben.
Ihr Datentyp enthält willkürliche Funktionstypen - die Sie nicht serialisieren können - Sie können also nicht hoffen, dass Sie die gewünschte Schnittstelle erhalten. 'saveState' ist nutzlos ohne die Fähigkeit, den Graphen selbst zu serialisieren. Der erste Schritt besteht darin, zu identifizieren, was Sie eigentlich modellieren möchten - die einzigen Funktionen, die Sie in einer Edge-Funktion verwenden, sind die konstante Funktion und "if". Wenn dies für Ihren allgemeinen Anwendungsfall repräsentativ ist, dann wird dies wahrscheinlich modelliert ziemlich leicht. Wenn Sie wirklich ein "Diagramm" (Knoten und Kanten) mit den zusätzlichen Sicherheitsbeschränkungen modellieren möchten, ist dies schwieriger. – user2407038
Ich suche nach einer allgemeinen Lösung. Ich kann mir komplexere Edge's vorstellen, die abhängig vom aktuellen Zustand 's' und der neuesten Antwort' a' den nächsten Untergraphen auswählen. Ein echtes Leben 'Edge' könnte sogar Datenbankverbindungen usw. verwenden und den Untergraphen in ein 'IO (Edge s' sf)' zurückversetzen. – homam
Man wählt nicht aus, welcher Knoten in einem Graphen "gehen" soll - jeder Knoten ist einfach mit einem (möglicherweise leeren) Satz von Knoten verbunden. Die * Wert Semantik * eines Knotens, der irgendwie einen Wert "erzeugt", auf dem der Übergang stattfindet, und der Übergang selbst sind nicht Teil der Graphenstruktur - vielmehr haben Sie einfach einen Graph, dessen Knoten und Kanten durch Dinge gekennzeichnet sind, die Sie interpretieren (in Ihrer Domäne) "Zustände" und "Übergänge" sein. d.h.Ihre * Kante * ist 'e1 = Kante n1 [n2, n3]', aber Ihre * Kantenbeschriftung * ist die Funktion '\ b -> wenn b ...' - die * Form * dieser Grafik kann leicht serialisiert werden, auch wenn die Etiketten können nicht. – user2407038