Я пытаюсь создать в Haskell безопасный вопрос-ответный поток. Я моделирую QnA как ориентированный граф, похожий на FSM.
Каждый node на графике представляет вопрос:
data Node s a s' = Node {
question :: Question a,
process :: s -> a -> s'
}
s
- это входное состояние, a
- ответ на вопрос, а s'
- состояние вывода. Узлы зависят от входного состояния s
, что означает, что для обработки ответа мы должны быть в определенном состоянии раньше.
Question a
представляют собой простой вопрос/ответ, создающий ответ типа a
.
В качестве типа я имею в виду, например, для node Node2 :: si -> a -> s2
, если si
зависит от s1
, тогда все пути, заканчивающиеся на Node2
, должны проходить через node, который производит s1
сначала. (Если s1 == si
, то все предшественники Node2
должны создавать s1
).
Пример
QnA: на веб-сайте онлайн-покупок нам нужно задать размер тела пользователя и любимый цвет.
-
e1
: спросите пользователя, знают ли они их размер. Если да, перейдите кe2
в противном случае перейдите кe3
-
e2
: спросите размер пользователя и перейдите кef
, чтобы задать цвет. -
e3
: (пользователь не знает их размер), задайте вес пользователя и перейдите кe4
. -
e4
: (послеe3
) задайте высоту пользователя и вычислите их размер и перейдите кef.
-
ef
: спросите пользовательский цвет и закончите поток с результатомFinal
.
В моей модели Edge
соедините Node
друг с другом:
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
является конечным результатом QnA, который здесь: (Bool, Size, Color)
.
Состояние QnA в каждый момент может быть представлено кортежем: (s, EdgeId)
. Это состояние сериализуемо, и мы должны иметь возможность продолжить QnA, просто зная это состояние.
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 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
Полный код:
{-# 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 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
Для меня важно, чтобы края были безопасными для типов. Значение, например, некорректная привязка e2
к e3
должна быть ошибкой типа: e2 = Edge E2 n2 (const $ const ef)
в порядке e2 = Edge E2 n2 (const $ const e3)
должна быть ошибкой.
Я задал свои вопросы с помощью --TOOD
:
-
Учитывая мои критерии сохранения типов с типом,
Edge s sf
должен иметь переменную типа ввода (s
), то как я могу создать функциюgetEdge :: EdgeId -> Edge s sf
? -
Как создать функцию
respond
, которая задала текущее состояниеs
и текущий крайEdge s sf
, вернет либо конечное состояние (если текущее реброFinal
), либо следующее состояние и следующий край(s', Edge s' sf)
?
Мой дизайн Node s a s'
и Edge s sf
может быть просто неправильным. Я не должен придерживаться этого.