【发布时间】:2016-09-05 10:03:00
【问题描述】:
我正在尝试在 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结果结束流程。
在我的模型中,Edges 将Nodes 相互连接:
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函数?如果给定当前状态
s和当前边Edge s sf,我如何创建respond函数,它将返回最终状态(如果当前边是Final)或下一个状态和下一条边(s', Edge s' sf)?
我对@987654371@ 和Edge s sf 的设计可能完全是错误的。我不必坚持下去。
【问题讨论】:
-
你的数据类型包含任意函数类型——你不能序列化——所以你不能希望在这里得到你想要的接口。
saveState没有序列化图形本身的能力是无用的。第一步是确定您实际希望建模的内容 - 您在“边缘”函数中使用的唯一函数是常量函数和if,如果这在您的一般用例中具有代表性,那么建模可能是很容易。如果你真的想用额外的类型安全约束来建模一个“图”(节点和边),那么这就更难了。 -
我正在寻找一个通用的解决方案。我可以想象更复杂的
Edges,它根据当前状态s和最新答案a选择下一个子图。现实生活中的Edge甚至可能使用数据库连接等,并在IO (Edge s' sf)中返回子图。 -
一个人不会在图中“选择”要“转到”哪个节点 - 每个节点都只是简单地连接到一组(可能是空的)节点。节点的 值语义 以某种方式“产生”要转换的值,以及转换本身不是图结构的一部分 - 而你只是有一个图,其节点和边标记为您将(在您的域中)解释为“状态”和“转换”的事物。即您的 edge 是
e1 = Edge n1 [n2,n3]但您的 edge label 是函数\b -> if b ...- 此图的 shape 可以轻松序列化,即使标签不能。 -
在
e1 = Edge n1 [n2,n3]n2和n3可能不是同一类型,它们有相同的输入但不同的输出。 (但n1 >>>= n2 >>>= nf和n1 >>>= n3 >>>= n4 >>>= nf是同一类型,输入输出相同)。