I am trying to create a type-safe Question-Answer flow in Haskell. I am modeling QnA as a directed graph, similar to a FSM.
Each node in the graph represent a question:
data Node s a s' = Node {
question :: Question a,
process :: s -> a -> s'
}
s
is the input state, a
is the answer to the question and s'
is the output state. Nodes depend on the input state s
, meaning that for processing the answer we have to be a in a particular state before.
Question a
represent a simple question / answer producing an answer of type a
.
By type-safe I mean, for example given a node Node2 :: si -> a -> s2
, if si
depends on s1
then all the paths ending with Node2
must be passing through a node that produces s1
first. (If s1 == si
then all predecessors of Node2
must be producing s1
).
An Example
QnA: In an online shopping website, we need to ask user's body size and favorite color.
e1
: ask user if they know their size. If yes then go toe2
otherwise go toe3
e2
: ask user's size and go toef
to ask the color.e3
: (user doesn't know their size), ask user's weight and go toe4
.e4
: (aftere3
) ask user's height and calculate their size and go toef.
ef
: ask user's favorite color and finish the flow with theFinal
result.
In my model, Edge
s connect Node
s to each other:
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
is the final result of the QnA, that here is: (Bool, Size, Color)
.
The QnA state at each moment can be represented by a tuple: (s, EdgeId)
. This state is serializable and we should be able to continue a QnA by just knowing this state.
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
Full 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
It's important for me to keep the edges type-safe. Meaning for instance incorrectly linking e2
to e3
must be a type error: e2 = Edge E2 n2 (const $ const ef)
is fine by e2 = Edge E2 n2 (const $ const e3)
must be an error.
I have indicated my questions with --TOOD
:
Given my criteria for keeping edges type-safe,
Edge s sf
must have an input type variable (s
) then how can I creategetEdge :: EdgeId -> Edge s sf
function?How can I create the
respond
function that given the current states
and current edgeEdge s sf
, will return either the final state (if current edge isFinal
) or the next state and the next edge(s', Edge s' sf)
?
My design of Node s a s'
and Edge s sf
might be simply wrong. I don't have to stick with it.