Я пытаюсь создать безопасный вопрос-ответ в Haskell. Я моделирую QnA как ориентированный граф, аналогичный FSM.Тип-безопасный поток (машина состояния)
Каждый узел в графе представляют собой вопрос:
data Node s a s' = Node {
question :: Question a,
process :: s -> a -> s'
}
s
является состояние входного сигнала, a
является ответом на вопрос, и s'
это состояние выхода. Узлы зависят от входного состояния s
, а это означает, что для обработки ответа мы должны быть в определенном состоянии раньше.
Question a
представляют собой простой вопрос/ответ, дающий ответ типа a
.
По типобезопасному я имею в виду, например, данный узел Node2 :: si -> a -> s2
, если si
зависит от s1
то все пути, заканчивающихся Node2
должны быть проходящим через узел, который производит s1
первые. (Если s1 == si
, то все предшественники Node2
должны производить s1
).
Пример
QnA: В интернет-сайт покупок, мы должны спросить пользователя размер тела и любимый цвет.
e1
: спросите пользователя, знают ли они их размер. Если да, то перейдите по ссылкеe2
в противном случае перейдите по ссылкеe3
e2
: запросите размер пользователя и перейдите на страницуef
, чтобы задать цвет.e3
: (пользователь не знает их размера), спросите вес пользователя и перейдите кe4
.e4
: (послеe3
) задать высоту пользователя и рассчитать их размер и перейти кef.
ef
: задать любимый цвет пользователя и закончить поток сFinal
результата.
В моей модели, Edge
s подключить Node
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
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'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
Полный код:
{-# 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
Это важно для меня, чтобы держать типобезопасный края.Значение, например, некорректная привязка 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
может быть просто неправильно. Я не должен придерживаться этого.
Ваш тип данных содержит произвольные типы функций, которые вы не можете сериализовать, поэтому вы не можете надеяться получить интерфейс, который вы хотите здесь. 'saveState' бесполезен без возможности сериализации самого графика. Первый шаг состоит в том, чтобы определить, что вы на самом деле хотите моделировать - единственными функциями, которые вы используете в функции «edge», являются константа и 'if', и если это является представительным, если ваш общий прецедент, то моделирование этого, вероятно, будет вполне легко. Если вы действительно хотите смоделировать «график» (узлы и ребра) с дополнительными ограничениями безопасности типа, это будет более жестким. – user2407038
Я ищу общее решение. Я могу представить более сложные «Edge's», которые выбирают следующий подграф в зависимости от текущего состояния 's' и последнего ответа' a'. Реальная жизнь 'Edge' может даже использовать соединения с базой данных и т. Д. И возвращать подграф в' IO (Edge s 'sf) '. – homam
Нельзя выбрать «какой» узел для перехода в граф - каждый узел просто связан с (возможно, пустым) набором узлов. Семантика * значения * узла каким-то образом «производит» значение, на которое нужно перейти, и сам переход, не являются частью структуры графа, а скорее у вас просто есть граф, чьи узлы и ребра помечены вещами, которые вы интерпретируете (в вашем домене), чтобы быть «состояниями» и «переходами». то естьваш * край * является 'e1 = Edge n1 [n2, n3]', но ваша метка * edge - это функция '\ b ->, если b ...' - * форма * этого графа может быть сериализована легко, даже если ярлыки не могут. – user2407038