【问题标题】:Composing Haskell tree structures组成 Haskell 树结构
【发布时间】:2019-11-09 22:22:20
【问题描述】:

我在 Haskell 中递归思考时遇到了问题。

我正在尝试构建一个调查应用程序,在该应用程序中,问题会根据用户的回答有条件地引发新问题。

我得到了:
- Questions - 问题列表
- QuestionPaths - 导致新问题的问题路径列表
- Answers 用户答案列表

您可以将QuestionPaths 视为一个元组列表,其中:

type QuestionPath = (QuestionId, AnswerChoice, NextQuestionId)

基本上会这样写:如果用户回答问题 QuestionId 有答案 AnswerChoice问他 NextQuestionId 下一个。

我尝试使用multiway trees 对这个问题域建模(节点可以有多个子节点):

data YesNo = Yes | No
data AnswerChoice =   Skip
                    | Boolean YesNo
                    | ChooseOne [Text]

type Condition = AnswerChoice

data QuestionTree = QuestionNode {
      question      :: Question
    , condition     :: Condition
    , userAnswer    :: Maybe AnswerChoice
    , children      :: QuestionForest
    }

type QuestionForest = [QuestionTree]

不幸的是,我现在对如何编写构成这样的树的算法一无所知。

我基本上需要这些函数来组合和遍历:

-- Constructs the tree from seed data
constructTree :: Questions -> QuestionPaths -> Answers -> QuestionTree

-- | Inserts answer to question in the tree
answerQuestion :: Question -> AnswerChoice

-- | Fetches the next unanswered question by traversing the tree.
getNextUnanswered :: QuestionTree -> Question

能否请您帮助我了解构建和遍历此类树的最佳方法是什么?

【问题讨论】:

  • 地图可能更合适:type QuestionTree = Data.Map.Map (QuestionID, AnswerChoice) QuestionID.
  • Map QuestionID (Map AnswerChoice QuestionID) 用于“咖喱”版本。
  • @chepner 那将如何替换树?我的意思是树是要构建的结构,以便可以按顺序向用户询问一系列条件问题。比如“你去健身房了吗?”是 -> “你在健身房锻炼了多长时间?”
  • @Hopia 试试看。

标签: haskell recursion tree


【解决方案1】:

在这种情况下,我要做的是将答案存储在单独的数据结构中 - 不是 将它们插入问题树中;将答案放在单独的列表/集合中,或者放在文件或数据库中,并让问题树是不可变的。

为了跟踪哪些问题还有待问,您可以“使用”树 - 保持程序状态指向下一个问题,丢弃已回答的问题(让垃圾收集器回收它们)。

我会这样设计树:

data AllowedAnswers = YesOrNo {
                        ifUserAnsweredYes :: QuestionTree,
                        ifUserAnsweredNo :: QuestionTree
                      }
                      | Choices [(Text, QuestionTree)]

data QuestionTree = Question {
      description :: Text
    , allowedAnswers :: AllowedAnswers
    , ifUserSkipsThisQuestion :: QuestionTree
  }
  | EndOfQuestions

注意几件事:

  • 您不必担心导致同一个问题的多个可能路径 - 您可以将同一个 QuestionTree 节点放置在多个位置,并且它将被共享(Haskell 不会创建它的多个副本)

  • 这种设计没有地方保存用户的答案 - 它们存储在其他地方(即某处的列表或文件) - 无需改变问题树。

  • 当用户回答问题时,只需将“指针”移动到下一个 QuestionTree,具体取决于用户回答的内容。

至于“如何从(QuestionId,AnswerChoice,NextQuestionId)列表中构造这棵树”-我想我首先将其转换为地图:``Map QuestionId [(AnswerChoice,Maybe QuestionId)],然后我将从第一个问题的 ID 开始构建树,并从 Map 中获取其直接子级,然后构建子树。

示例(对于非常简化的情况,其中唯一可能的答案是“是”或“否”,不允许跳过):

buildTree questionMap questionId = case Map.lookup questionId questionMap of
  Nothing -> EndOfQuestions
  Just (description, [("yes", nextQuestionIdIfYes), ("no", nextQuestionIdIfNo)]) ->
    Question { description = description
               , allowedAnswers = YesOrNo {
                   ifUserAnsweredYes = buildTree questionMap nextQuestionIdIfYes
                   , ifUserAnsweredNo = buildTree questionMap nextQuestionIdIfNo
                 }
               , ifUserSkipsThisQuestion = EndOfQuestions
             }

如果您想知道“为什么不直接使用地图?” - 是的,您可以(而且通常这是正确的解决方案),但请考虑:

  • QuestionTree 结构比 Id 映射更惯用地表达了程序员的意图 -> Thing

  • 只要相关,结构上就可以保证有一个子 QuestionTree - 无需执行 Map.lookup,它将返回一个 Maybe,您必须验证它是否包含 Just(即使您知道 em> 会有下一个问题,即使是 EndOfQuestions)

【讨论】:

  • +1 是一个好方法。请注意,尽管 数据结构 允许问题节点共享,但 buildTree 实现的工作方式,重复问题的问题节点不会共享,但这可能是不值得“修复”,除非您在同一棵树上处理大量已完成的问卷,否则持久性树和性能会成为问题。
  • @ka-buhr 是的,确保共享的理想方法是 1) 找到叶节点和 2),然后自下而上构建树,但这太复杂了,不适合在一个答案中(特别是因为 OP 仍然围绕着递归)
  • 非常感谢您通过实际示例设计的解决方案!我现在正在试验它。我实际上是在生成可能的答案选择,并直接从数据库中动态映射其后续的后续问题。此外,一个问题的单个答案可能会导致多个(同级)后续问题。所以它使控制流有些复杂
  • 由于您使用此解决方案解决了我与树构建相关的固有递归问题,因此我将其标记为已接受的答案。感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2014-07-28
  • 2016-02-24
  • 1970-01-01
  • 1970-01-01
  • 2018-05-25
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多